标题:使用 AspectJ 实现对带注解成员变量的读写访问拦截

本文介绍如何通过 aspectj 的 `get()` 和 `set()` 切点表达式,精准拦截被自定义注解(如 `@monitor`)标记的类成员变量的读写操作,并提供可运行的完整示例与关键注意事项。

在面向切面编程中,若希望对某个字段的所有读写访问(无论发生在 getter/setter 内部、普通方法中,还是直接访问)进行统一监控,仅靠拦截 getter/setter 方法执行是不够的——因为 Java 字节码层面的字段访问(getfield/putfield)并不经过方法调用。此时,AspectJ 提供了更底层的 get() 和 set() 切点(pointcut),可直接匹配字段访问行为,且支持结合注解进行条件筛选。

✅ 正确的切点写法

要拦截被 @Monitor 注解修饰的任意字段的读写操作,应使用如下 AspectJ 切点语法:

@Around("get(@Monitor * *)")
public Object onFieldRead(ProceedingJ

oinPoint jp) throws Throwable { System.out.println("[READ] " + jp.getSignature()); return jp.proceed(); } @Around("set(@Monitor * *)") public Object onFieldWrite(ProceedingJoinPoint jp) throws Throwable { System.out.println("[WRITE] " + jp.getSignature()); return jp.proceed(); }
  • get(@Monitor * *):匹配所有被 @Monitor 注解修饰的字段的读取操作(即 getfield 字节码指令);
  • set(@Monitor * *):匹配所有被 @Monitor 注解修饰的字段的写入操作(即 putfield 指令);
  • * * 分别代表任意类型任意名称的字段,等价于 @Monitor @interfaceType fieldName 的通配。
⚠️ 注意:@Around("annotation(Monitor) && ...") 是错误思路——annotation() 切点仅适用于连接点本身带有该注解(如方法、类、参数),而字段读写操作(get/set)本身不是注解目标,字段才是。因此必须使用 get(@Monitor ...) 这种“目标字段带注解”的语义。

? 完整可运行示例

首先定义注解(需 RUNTIME 保留策略):

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
public @interface Monitor {}

目标类(字段直接标注,无需 getter/setter 也生效):

public class Point {
    @Monitor
    private int x;

    public int getX() { return x; }
    public void setX(int v) { x = v; }

    public static void main(String[] args) {
        Point p = new Point();
        p.setX(11);           // → 触发 set(int Point.x)
        System.out.println(p.getX()); // → 触发 get(int Point.x)

        p.x = 22;             // → 直接触发 set(int Point.x)
        System.out.println(p.x);      // → 直接触发 get(int Point.x)
    }
}

切面实现(注意:需启用 AspectJ 编译时织入或加载时织入):

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;

@Aspect
public class MonitorAspect {

    @Around("get(@Monitor * *)")
    public Object onFieldRead(ProceedingJoinPoint jp) throws Throwable {
        System.out.printf("[READ] %s%n", jp.getSignature());
        return jp.proceed();
    }

    @Around("set(@Monitor * *)")
    public Object onFieldWrite(ProceedingJoinPoint jp) throws Throwable {
        System.out.printf("[WRITE] %s%n", jp.getSignature());
        return jp.proceed();
    }
}

运行输出将清晰显示四次拦截(两次通过方法、两次直接访问):

[WRITE] set(int Point.x)
[READ] get(int Point.x)
11
[WRITE] set(int Point.x)
[READ] get(int Point.x)
22

? 补充说明与注意事项

  • 织入方式要求:get()/set() 切点属于 field-access join points,仅支持 编译时织入(ajc)加载时织入(LTW),标准 Spring AOP(基于代理)无法支持,因其不操作字节码层面的字段访问。
  • 性能影响:字段级切点会增加所有匹配字段访问的开销,生产环境建议谨慎评估粒度,必要时配合 if() 切点条件过滤。
  • 访问控制无关:即使字段是 private,AspectJ 仍可拦截其读写(依赖字节码增强),无需 AccessibleObject.setAccessible(true)。
  • 扩展建议:如需区分 getter/setter 调用上下文,可结合 jp.getEnclosingStaticPart() 获取调用方方法签名(见原答案中 EnclosingStaticPart 示例);如需获取字段值,jp.getArgs() 对 set() 有效(含新值),jp.proceed() 后对 get() 可捕获返回值。

掌握 get(@Annotation ...) 与 set(@Annotation ...) 是解锁 AspectJ 字段级横切能力的关键——它让监控真正下沉到数据访问本质,而非停留于方法契约表面。