如何使用 AspectJ 实现对带 @Monitor 注解字段的读写访问拦截

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

在 Spring AOP 中,无法直接拦截字段访问(field access),因为其基于代理机制,仅支持方法调用拦截;而 AspectJ(尤其是编译时织入或加载时织入)则原生支持对字段读写(get() / set())的切点定义。这意味着你可以真正实现“只要某字段被 @Monitor 标记,无论它是在 getter/setter 中被访问,还是在任意位置被直接读写”,都能触发对应增强逻辑。

✅ 正确的切点写法

要匹配被 @Monitor 注解修饰的任意字段的读/写操作,应使用 AspectJ 特有的 get() 和 set() 切点指示符:

@Around("get(@Monitor * *)")
public Object onFieldRead(ProceedingJoinPoint 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 注解修饰的字段的读取操作(如 obj.x、return this.x;);
  • set(@Monitor * *):匹配所有被 @Monitor 注解修饰的字段的写入操作(如 obj.x = 1、this.x = v;);
  • 第一个 * 表示任意返回类型(即字段类型),第二个 * 表示任意字段名 —— 组合起来即“任意类型、任意名称、且带 @Monitor 注解的字段”。
⚠️ 注意:@annotation(Monitor) 是用于匹配被注解的方法/类型/参数,不能用于字段访问切点。字段级注解需结合 get() / set() 使用,语法为 get(@Monitor ...) 或 set(@Monitor ...)。

? 完整可运行示例

1. 自定义注解(必须保留至运行时)

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

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

2. 测试类(含直接与间接字段访问)

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(); // → 间接访问:触发 setter/getter 方法执行(但字段访问仍被拦截) p.setX(11); System.out.println(p.getX()); // 输出 11 // → 直接访问:绕过方法,但仍被 get/set 切点捕获 p.x = 22; System.out.println(p.x); // 输出 22 } }

3. AspectJ 切面(需启用 LTW 或 CTW)

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 onRead(ProceedingJoinPoint jp) throws Throwable {
        System.out.printf("[FIELD READ]  %s%n", jp.getSignature());
        return jp.proceed();
    }

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

运行输出(清晰区分访问来源):

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

? 进阶提示与注意事项

  • 织入方式要求:上述 get()/set() 切点不被 Spring AOP 支持,必须使用 AspectJ(推荐加载时织入 LTW,配合 aop.xml 和 JVM 参数 -javaagent:aspectjweaver.jar)。
  • 访问可见性无关:即使字段是 private,AspectJ 仍可拦截其读写(底层通过字节码改写实现)。
  • 性能影响:字段级拦截会增加运行时开销,建议仅对关键监控字段使用,并避免在高频循环中直接访问被监控字段。
  • 获取上下文信息:可通过 jp.getThis() 获取目标对象、jp.getArgs()(对 set() 有效,返回新值数组)、jp.getSignature().getDeclaringType() 等进一步定制逻辑。
  • 排除干扰:若只想拦截 getter/setter 内部的字段访问(而非任意位置),可组合 cflow() 切点,例如:
    @Around("set(@Monitor * *) && cflow(execution(* *.get*(..)) || execution(* *.set*(..)))")

掌握 get(@Annotation ...) 和 set(@Annotation ...) 是解锁 AspectJ 字段级横切能力的关键。它让监控、审计、懒加载、属性变更通知等场景变得简洁而强大 —— 只需注解字段,无需修改业务代码。