Spring AOP 限制与问题排查

本文是 Spring AOP 系列的边界总结篇,重点整理 Spring AOP 做不到什么、哪些场景容易失效、以及遇到 AOP 不生效时应该如何排查。
主线是:代理模型决定能力边界 -> 常见失效场景 -> 定位路径 -> 解决建议。


一、先建立主线:限制来自代理模型

Spring AOP 的优势是轻量、和 Spring IOC 集成紧密、适合处理方法级横切逻辑。

但它的限制也很明确:

Spring AOP 基于运行期代理对象工作,所以只有经过 Spring 代理对象的方法调用,才有机会被增强。

这句话决定了大部分限制:

  • 目标对象必须是 Spring 容器管理的 Bean。
  • 调用必须经过 Spring 暴露出来的代理对象。
  • 默认主要拦截方法调用,不是字段访问或构造器执行。
  • JDK 动态代理和 CGLIB 各自有类型限制。
  • 自调用、finalprivate、直接 new 对象都会导致增强失效。

遇到 AOP 不生效,第一步不要先怀疑注解,而是先问:

1
这次调用真的经过代理对象了吗?

二、限制一:只能增强 Spring Bean

Spring AOP 只能代理 Spring 容器中的 Bean。

如果对象是自己 new 出来的,Spring 根本没有机会为它创建代理。

错误示例:

1
2
3
4
5
6
7
public class Demo {

public void test() {
UserService userService = new UserService();
userService.createUser();
}
}

原因是:

1
2
3
4
new UserService()
-> 不经过 Spring 容器
-> 不经过 BeanPostProcessor
-> 不会创建代理对象

正确方式是从 Spring 容器注入:

1
2
3
4
5
6
7
8
9
@Service
public class DemoService {

private final UserService userService;

public DemoService(UserService userService) {
this.userService = userService;
}
}

三、限制二:调用必须经过代理对象

即使 Bean 已经被代理,如果调用绕过代理,也不会触发 AOP。

最典型的是自调用:

1
2
3
4
5
6
7
8
9
10
11
@Service
public class UserService {

public void outer() {
inner();
}

@Transactional
public void inner() {
}
}

调用链是:

1
2
3
4
调用方
-> 代理对象
-> outer()
-> this.inner()

this.inner() 是目标对象内部调用,没有重新经过代理对象。

常见解决方式:

方式 建议程度 说明
拆分到另一个 Bean 推荐 结构最清晰,最符合 Spring 代理模型
注入自身代理 可用但谨慎 需要处理循环依赖,代码可读性一般
AopContext.currentProxy() 不推荐常规使用 强依赖 Spring AOP,需要开启 exposeProxy

四、限制三:默认是方法级代理

Spring AOP 主要拦截方法调用。

它不适合处理:

  • 字段读写拦截。
  • 构造器调用拦截。
  • 任意对象创建拦截。
  • 非 Spring 管理对象的调用。
  • 更底层的字节码织入场景。

例如字段赋值不会触发 AOP:

1
user.name = "Tom";

构造器内部调用也不会触发 AOP:

1
2
3
4
5
6
7
8
9
10
11
@Service
public class UserService {

public UserService() {
init();
}

@Transactional
public void init() {
}
}

原因是构造器执行时,代理对象还没有完成创建。

如果确实需要字段级、构造器级、对象创建级织入,应考虑 AspectJ 编译期织入或加载期织入,而不是 Spring AOP。


五、限制四:JDK 动态代理的类型限制

如果目标类实现了接口,Spring 默认可能使用 JDK 动态代理。

JDK 动态代理的特点是:

1
2
代理对象 implements 接口
代理对象不 extends 目标实现类

因此下面这种注入可能出问题:

1
private final UserServiceImpl userService;

因为代理对象可能只是:

1
class jdk.proxy2.$Proxy58 implements UserService

它不是 UserServiceImpl 的子类。

推荐写法是面向接口:

1
private final UserService userService;

如果确实需要基于类代理,可以强制 CGLIB:

1
2
3
spring:
aop:
proxy-target-class: true

但设计上仍优先推荐面向接口依赖。


六、限制五:CGLIB 的 final / private 限制

CGLIB 通过生成目标类子类实现代理。

它依赖两个前提:

  1. 目标类可以被继承。
  2. 目标方法可以被重写。

这些场景会有问题:

场景 原因
final 不能被继承,无法生成代理子类
final 方法 不能被重写,无法插入拦截逻辑
private 方法 子类不可见,也不能被外部代理调用
static 方法 属于类,不属于实例代理调用

示例:

1
2
3
4
5
6
7
@Service
public final class UserService {

@Transactional
public void createUser() {
}
}

如果需要 CGLIB 代理,final 类会阻碍代理生成。

方法级别也一样:

1
2
3
@Transactional
public final void createUser() {
}

即使代理对象存在,final 方法也无法被子类覆盖拦截。


七、限制六:Pointcut 没匹配上

很多 AOP 不生效,不是代理机制问题,而是切点表达式没有匹配目标方法。

常见问题:

问题 示例
包路径写错 com.demo.service 写成 com.demo.services
方法签名不匹配 只匹配 public,但目标方法不是 public
注解位置不对 匹配方法注解,但注解放在类上
代理类型影响 this JDK 代理下 this(实现类) 可能不匹配
Bean 名称不匹配 bean(orderService) 写错 BeanName

建议先用更宽的表达式验证切面是否能触发:

1
2
3
4
@Before("execution(* com.example..*(..))")
public void before() {
System.out.println("before");
}

如果宽表达式能触发,再逐步收窄范围。


八、限制七:多个切面顺序不符合预期

多个切面同时命中一个方法时,执行顺序由 Advisor 排序决定。

常见控制方式:

1
2
3
4
5
6
7
8
9
10
11
@Order(1)
@Aspect
@Component
public class FirstAspect {
}

@Order(2)
@Aspect
@Component
public class SecondAspect {
}

对于 @Around,顺序表现为洋葱模型:

1
2
3
4
5
First before
-> Second before
-> target method
-> Second after
-> First after

如果事务也参与其中,事务拦截器同样是拦截器链的一部分。


九、AOP 不生效排查总表

现象 常见原因 排查方式 建议解决
切面完全不执行 没启用 AOP 或切面没注册 检查依赖、@Aspect@Component 引入 AOP 依赖,确保组件扫描到切面
某个 Bean 不执行 Bean 没被代理 打印 bean.getClass(),检查 AopUtils.isAopProxy() 检查 Pointcut、Bean 是否由 Spring 管理
某个方法不执行 方法没匹配或绕过代理 检查 Pointcut,排查自调用 调整表达式或拆分 Bean
@Transactional 不生效 自调用、异常被吞、非 Spring Bean 看是否进入 TransactionInterceptor 拆分 Bean,保持异常传播
按实现类注入失败 JDK 动态代理不是实现类子类 看代理类是否 $Proxy 面向接口注入或强制 CGLIB
CGLIB 代理失败 final 类或方法 检查类和方法修饰符 去掉 final 或改设计
顺序不符合预期 Advisor 排序问题 查看 Advisor 链顺序 使用 @Order 或配置 order

十、推荐排查路径

遇到 AOP 不生效,建议按下面顺序排查:

1
2
3
4
5
6
7
8
9
1. 切面类是否是 Spring Bean
2. AOP 自动代理创建器是否注册
3. 目标对象是否是 Spring Bean
4. 目标 Bean 是否被代理
5. Pointcut 是否匹配目标方法
6. 调用是否经过代理对象
7. 方法是否可被代理
8. 拦截器链中是否有对应 MethodInterceptor
9. 异常或返回值语义是否被其他切面改变

常用调试代码:

1
2
3
4
5
System.out.println(bean.getClass());
System.out.println(AopUtils.isAopProxy(bean));
System.out.println(AopUtils.isJdkDynamicProxy(bean));
System.out.println(AopUtils.isCglibProxy(bean));
System.out.println(AopUtils.getTargetClass(bean));

常用断点:

1
2
3
4
AbstractAutoProxyCreator#wrapIfNecessary()
AbstractAdvisorAutoProxyCreator#findEligibleAdvisors()
DefaultAdvisorChainFactory#getInterceptorsAndDynamicInterceptionAdvice()
ReflectiveMethodInvocation#proceed()

如果是事务问题,再额外看:

1
2
TransactionInterceptor#invoke()
TransactionAttributeSourcePointcut#matches()

十一、什么时候不该用 Spring AOP

Spring AOP 适合:

  • 日志记录。
  • 权限校验。
  • 事务管理。
  • 缓存处理。
  • 幂等控制。
  • 简单性能统计。
  • 方法级审计。

不太适合:

  • 字段读写增强。
  • 构造器增强。
  • 非 Spring 对象增强。
  • 大量细粒度方法的性能敏感增强。
  • 复杂编译期织入场景。
  • 需要跨对象创建、字段访问、静态方法的全面织入。

如果需求超出 Spring AOP 能力边界,可以考虑:

需求 更合适方案
字段、构造器、对象创建织入 AspectJ 编译期 / 加载期织入
Web 请求统一处理 Filter / HandlerInterceptor
参数校验 Bean Validation / Method Validation
全链路追踪 Observability / Tracing 组件
安全控制 Spring Security

十二、和前面几篇的关系

本篇是边界总结,不重复展开源码细节。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
02-02-Spring-AOP-Proxy.md
-> 解释代理为什么导致自调用、final 等限制

02-03-Spring-AOP-AutoProxyCreator.md
-> 解释 Bean 为什么会或不会被自动代理

02-04-Spring-AOP-Advisor-Advice-Pointcut.md
-> 解释 Pointcut 为什么可能匹配不上

02-05-Spring-AOP-Invocation-Chain.md
-> 解释拦截器链、@Around、异常传播问题

02-06-Spring-AOP-And-Transaction.md
-> 解释 @Transactional 为什么有同样的代理限制

排查问题时,不要孤立看某个注解,要回到完整链路:

1
2
3
4
5
是否有 Advisor
-> 是否匹配 Bean
-> 是否创建代理
-> 调用是否经过代理
-> 拦截器链是否执行

十三、面试高频快答

Spring AOP 最大限制是什么?

Spring AOP 基于代理对象工作,只有 Spring 容器管理的 Bean,并且调用经过代理对象时,增强才会生效。

为什么自调用会失效?

因为自调用是目标对象内部通过 this 调用,没有经过 Spring 暴露的代理对象。

为什么非 Spring Bean 不生效?

因为对象没有经过 Spring 容器创建和 BeanPostProcessor 处理,Spring 没机会为它生成代理。

为什么 final 方法不能被 CGLIB 增强?

CGLIB 依赖子类重写目标方法来拦截调用,final 方法不能被重写,所以无法增强。

为什么 JDK 动态代理按实现类注入可能失败?

JDK 动态代理生成的是接口代理对象,不继承目标实现类,所以不能当作实现类注入。

AOP 不生效第一步排查什么?

先确认目标 Bean 是否是代理对象,再确认 Pointcut 是否匹配,最后确认调用是否经过代理。


十四、本篇总结

Spring AOP 的限制都可以回到代理模型解释:

1
2
3
4
5
Spring Bean
-> 自动代理创建器判断 Advisor
-> 创建代理对象
-> 调用经过代理
-> 拦截器链执行增强

任何一个环节断掉,AOP 都可能不生效。

必须掌握的点:

  • Spring AOP 只能增强 Spring 管理的 Bean。
  • 调用必须经过代理对象,自调用会绕过代理。
  • Spring AOP 默认主要处理方法级增强。
  • JDK 动态代理和 CGLIB 都有各自限制。
  • 事务失效本质上也是 AOP 代理链路失效。
  • 排查 AOP 问题要沿着 Advisor、代理对象、调用链三个方向定位。