适用人群

  • 知道 Spring AOP 是啥
  • 写过 @Around / @Before
  • 但对「注解是怎么传进 Advice 的」有点懵

这篇文章不默认你已经懂,我们从「注解怎么写」开始。


0️⃣ 先准备一个“被切的注解”(否则一切都是空谈)

1
2
3
4
5
6
7
8
9

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Auth {

String value() default "";

boolean required() default true;
}

关键点(只记 2 个):

  • @Retention(RUNTIME)

    没这个,AOP 在运行时根本拿不到注解

  • METHOD / TYPE

    决定你后面能不能用 @annotation / @within


1️⃣ 再看一个“被切的方法”(完整上下文)

1
2
3
4
5

@Auth("admin")
public void createOrder() {
// business logic
}

现在场景明确了:

  • 方法上有 @Auth
  • AOP 想在运行时 拿到这个注解里的配置

2️⃣ 最容易让人懵的地方:两种“几乎一样”的写法

❌ 写法一:只能“切到”,拿不到注解

1
2
3
4
5

@Around("@annotation(Auth)")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
return pjp.proceed();
}

它的真实含义是:

只要方法上“存在” @Auth,我就切

✔ 能切
❌ 但你拿不到 @Auth 实例

原因很简单:

你只是问了“有没有”,
没说“拿给我”。


✅ 写法二:切 + 绑定 + 传参(正确姿势)

1
2
3
4
5
6
7

@Around("@annotation(auth)")
public Object around(ProceedingJoinPoint pjp, Auth auth) throws Throwable {
// auth 就是方法上的 @Auth 注解实例
String role = auth.value();
return pjp.proceed();
}

这里发生了三件事:

  1. @annotation(auth)

    • auth 是变量名,不是类型
  2. Spring 从目标方法上

    • 拿到 @Auth 注解实例
  3. 注入到参数 Auth auth

👉 这一步,叫 参数绑定(binding)


3️⃣ 一个万能记忆口诀(真的有用)

切点里写“类型” → 只判断有没有
切点里写“变量名” → 才能把值传进来

写法 含义
@annotation(Auth) 存在性判断
@annotation(auth) 绑定注解实例
args(id) 绑定方法参数
target(service) 绑定目标对象

4️⃣ 进阶:方法注解 + 类注解一起用怎么办?(千万别踩坑)

这是 AOP 的 真·进阶区

先给结论(重点):
同一个注解同时出现在「方法」和「类」上时,Spring AOP 不会自动帮你选“谁优先”。

你必须在切面里自己定规则。也就是说——“优先级不是注解决定的,是你代码决定的。”

❌ 错误的依赖(不要赌)

1
2
3
4
5
6
@Around("@annotation(auth) || @within(auth)")
public Object around(ProceedingJoinPoint pjp, Auth auth) throws Throwable {
// ⚠️ 如果方法和类上都有 @Auth,这里的 auth 到底是谁?
// 答案:不确定,依赖匹配顺序,不可靠,不该赌。
return pjp.proceed();
}

✅ 正确姿势:你自己决定谁优先(标准写法)

业界共识规则(强烈推荐):方法级注解 > 类级注解
这和 Spring Security、事务传播规则是一致的。

写法思路

  1. 切面只负责“兜底捕获”(我是雷达,发现目标)。
  2. 代码负责“解析优先级”(我是指挥官,决定打谁)。

第一步:切面只“兜底”

1
2
3
4
5
6
7
8
// 只负责命中,不负责绑定具体参数,避免歧义
@Around("@within(com.xxx.Auth) || @annotation(com.xxx.Auth)")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
// 手动解析,掌控全场
Auth auth = resolveAuth(pjp);
// ... 业务逻辑
return pjp.proceed();
}

第二步:自己解析优先级(核心)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private Auth resolveAuth(ProceedingJoinPoint pjp) {
MethodSignature signature = (MethodSignature) pjp.getSignature();
Method method = signature.getMethod();

// 1️⃣ 优先拿方法上的
Auth methodAuth = method.getAnnotation(Auth.class);
if (methodAuth != null) {
return methodAuth;
}

// 2️⃣ 再拿类上的
Class<?> targetClass = pjp.getTarget().getClass();
return targetClass.getAnnotation(Auth.class);
}

🎯 为什么要这么麻烦?

这个方案的优点非常重要:

  • 行为确定:不再依赖 Spring 的内部匹配顺序。
  • 规则显式:新人看代码一眼就知道“方法优先”。
  • 可扩展:如果你以后想支持 disable / inherit / merge 策略,改 resolveAuth 就行,切面不用动。

💡 工程师级总结

  • @within / @annotation雷达:告诉你“都在”。
  • resolveAuth 才是 指挥官:告诉你“先打谁”。

5️⃣ 常见误区(90% 的坑都在这)

❌ 误区 1:Pointcut 参数 = 普通方法参数

实际:它是上下文占位符

❌ 误区 2:Advice 里多写一个参数,Spring 会自动给

实际:没绑定来源,直接启动报错

❌ 误区 3:Authauth 没区别

实际:

  • Auth:类型判断
  • auth:变量绑定

6️⃣ 最后一段人话总结

AOP 参数不是“注入”的,
是你在切点里“声明来源”,
Spring 再把上下文对象塞给你。

写对了是自动挡,
写错了是离合踩到底还挂不上档。


TL;DR(真·一句话)

切点里不绑定变量名,Advice 里就永远拿不到参数。

——完