02-07-Spring-AOP-Limitations
Spring AOP 限制与问题排查
本文是 Spring AOP 系列的边界总结篇,重点整理 Spring AOP 做不到什么、哪些场景容易失效、以及遇到 AOP 不生效时应该如何排查。
主线是:代理模型决定能力边界 -> 常见失效场景 -> 定位路径 -> 解决建议。
一、先建立主线:限制来自代理模型
Spring AOP 的优势是轻量、和 Spring IOC 集成紧密、适合处理方法级横切逻辑。
但它的限制也很明确:
Spring AOP 基于运行期代理对象工作,所以只有经过 Spring 代理对象的方法调用,才有机会被增强。
这句话决定了大部分限制:
- 目标对象必须是 Spring 容器管理的 Bean。
- 调用必须经过 Spring 暴露出来的代理对象。
- 默认主要拦截方法调用,不是字段访问或构造器执行。
- JDK 动态代理和 CGLIB 各自有类型限制。
- 自调用、
final、private、直接new对象都会导致增强失效。
遇到 AOP 不生效,第一步不要先怀疑注解,而是先问:
1 | 这次调用真的经过代理对象了吗? |
二、限制一:只能增强 Spring Bean
Spring AOP 只能代理 Spring 容器中的 Bean。
如果对象是自己 new 出来的,Spring 根本没有机会为它创建代理。
错误示例:
1 | public class Demo { |
原因是:
1 | new UserService() |
正确方式是从 Spring 容器注入:
1 | @Service |
三、限制二:调用必须经过代理对象
即使 Bean 已经被代理,如果调用绕过代理,也不会触发 AOP。
最典型的是自调用:
1 | @Service |
调用链是:
1 | 调用方 |
this.inner() 是目标对象内部调用,没有重新经过代理对象。
常见解决方式:
| 方式 | 建议程度 | 说明 |
|---|---|---|
| 拆分到另一个 Bean | 推荐 | 结构最清晰,最符合 Spring 代理模型 |
| 注入自身代理 | 可用但谨慎 | 需要处理循环依赖,代码可读性一般 |
AopContext.currentProxy() |
不推荐常规使用 | 强依赖 Spring AOP,需要开启 exposeProxy |
四、限制三:默认是方法级代理
Spring AOP 主要拦截方法调用。
它不适合处理:
- 字段读写拦截。
- 构造器调用拦截。
- 任意对象创建拦截。
- 非 Spring 管理对象的调用。
- 更底层的字节码织入场景。
例如字段赋值不会触发 AOP:
1 | user.name = "Tom"; |
构造器内部调用也不会触发 AOP:
1 | @Service |
原因是构造器执行时,代理对象还没有完成创建。
如果确实需要字段级、构造器级、对象创建级织入,应考虑 AspectJ 编译期织入或加载期织入,而不是 Spring AOP。
五、限制四:JDK 动态代理的类型限制
如果目标类实现了接口,Spring 默认可能使用 JDK 动态代理。
JDK 动态代理的特点是:
1 | 代理对象 implements 接口 |
因此下面这种注入可能出问题:
1 | private final UserServiceImpl userService; |
因为代理对象可能只是:
1 | class jdk.proxy2.$Proxy58 implements UserService |
它不是 UserServiceImpl 的子类。
推荐写法是面向接口:
1 | private final UserService userService; |
如果确实需要基于类代理,可以强制 CGLIB:
1 | spring: |
但设计上仍优先推荐面向接口依赖。
六、限制五:CGLIB 的 final / private 限制
CGLIB 通过生成目标类子类实现代理。
它依赖两个前提:
- 目标类可以被继承。
- 目标方法可以被重写。
这些场景会有问题:
| 场景 | 原因 |
|---|---|
final 类 |
不能被继承,无法生成代理子类 |
final 方法 |
不能被重写,无法插入拦截逻辑 |
private 方法 |
子类不可见,也不能被外部代理调用 |
static 方法 |
属于类,不属于实例代理调用 |
示例:
1 | @Service |
如果需要 CGLIB 代理,final 类会阻碍代理生成。
方法级别也一样:
1 | @Transactional |
即使代理对象存在,final 方法也无法被子类覆盖拦截。
七、限制六:Pointcut 没匹配上
很多 AOP 不生效,不是代理机制问题,而是切点表达式没有匹配目标方法。
常见问题:
| 问题 | 示例 |
|---|---|
| 包路径写错 | com.demo.service 写成 com.demo.services |
| 方法签名不匹配 | 只匹配 public,但目标方法不是 public |
| 注解位置不对 | 匹配方法注解,但注解放在类上 |
代理类型影响 this |
JDK 代理下 this(实现类) 可能不匹配 |
| Bean 名称不匹配 | bean(orderService) 写错 BeanName |
建议先用更宽的表达式验证切面是否能触发:
1 | @Before("execution(* com.example..*(..))") |
如果宽表达式能触发,再逐步收窄范围。
八、限制七:多个切面顺序不符合预期
多个切面同时命中一个方法时,执行顺序由 Advisor 排序决定。
常见控制方式:
1 | @Order(1) |
对于 @Around,顺序表现为洋葱模型:
1 | First before |
如果事务也参与其中,事务拦截器同样是拦截器链的一部分。
九、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 | 1. 切面类是否是 Spring Bean |
常用调试代码:
1 | System.out.println(bean.getClass()); |
常用断点:
1 | AbstractAutoProxyCreator#wrapIfNecessary() |
如果是事务问题,再额外看:
1 | TransactionInterceptor#invoke() |
十一、什么时候不该用 Spring AOP
Spring AOP 适合:
- 日志记录。
- 权限校验。
- 事务管理。
- 缓存处理。
- 幂等控制。
- 简单性能统计。
- 方法级审计。
不太适合:
- 字段读写增强。
- 构造器增强。
- 非 Spring 对象增强。
- 大量细粒度方法的性能敏感增强。
- 复杂编译期织入场景。
- 需要跨对象创建、字段访问、静态方法的全面织入。
如果需求超出 Spring AOP 能力边界,可以考虑:
| 需求 | 更合适方案 |
|---|---|
| 字段、构造器、对象创建织入 | AspectJ 编译期 / 加载期织入 |
| Web 请求统一处理 | Filter / HandlerInterceptor |
| 参数校验 | Bean Validation / Method Validation |
| 全链路追踪 | Observability / Tracing 组件 |
| 安全控制 | Spring Security |
十二、和前面几篇的关系
本篇是边界总结,不重复展开源码细节。
1 | 02-02-Spring-AOP-Proxy.md |
排查问题时,不要孤立看某个注解,要回到完整链路:
1 | 是否有 Advisor |
十三、面试高频快答
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 | Spring Bean |
任何一个环节断掉,AOP 都可能不生效。
必须掌握的点:
- Spring AOP 只能增强 Spring 管理的 Bean。
- 调用必须经过代理对象,自调用会绕过代理。
- Spring AOP 默认主要处理方法级增强。
- JDK 动态代理和 CGLIB 都有各自限制。
- 事务失效本质上也是 AOP 代理链路失效。
- 排查 AOP 问题要沿着 Advisor、代理对象、调用链三个方向定位。
