Spring 的 AOP 在什么场景下会失效?
结论先行:AOP 失效只有一个根因——调用没有经过代理对象,或者代理没能生成/命中。前者包括同类自调用、this 调用、内部类调用;后者包括方法不可覆写(private/final/static)、对象不是 Spring Bean、切点表达式没匹配上、JDK 代理类型不匹配。排查顺序固定:先确认"有没有代理",再确认"代理有没有被调用到"。
一、失效清单
| 场景 | 原因 | 解决 |
|---|---|---|
| 同类方法自调用 | this.b() 不经过代理 | 拆到另一个 Bean、自注入、AopContext.currentProxy() |
private / final / static 方法 | CGLIB 无法覆写、静态方法不参与代理 | 改为 public 非 final,或把逻辑挪到可代理方法 |
new 出来的对象 | 不在容器里,不会被代理 | 交给容器管理(@Component 或 @Bean) |
| JDK 代理 + 注入实现类 | 代理只实现接口,不是实现类 | 注入接口,或 proxyTargetClass=true 用 CGLIB |
| 切点表达式写错 | 包路径/返回值/参数不匹配 | @Pointcut 单独抽出来,开 debug 看匹配 |
| 注解标在接口上,实现类无注解 | 默认不继承接口方法上的注解 | 注解标在实现类,或用 AspectJ 模式 |
@Transactional 标在 Controller | 事务只加在 Service 层才合理,且需代理 | 移到 Service,并确保是外部调用 |
| 内部类 / 匿名类调用外部 Bean 方法 | 视引用而定,常是 this 语义 | 用注入的引用调用 |
二、自调用示意
外部调用 ──▶ 代理(有事务/日志) ──▶ 目标对象 a()
│
└─ this.b() ← 直接走原始对象,绕开代理
b() 上的 @Transactional 完全不生效
@Service
public class OrderService {
@Transactional
public void create() {
// 反例:自调用,save 上的事务注解不生效
this.save();
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void save() { /* ... */ }
}
三、三种修复写法
// 方案一:拆类(最推荐)。把 save 放到独立 Bean,跨 Bean 调用必过代理
@Service
public class OrderSaveService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void save() { /* ... */ }
}
// 方案二:自注入。容器注入的是代理,通过它调用就会走通知链
@Service
public class OrderService {
@Autowired
@Lazy // 避免自注入引发循环依赖告警
private OrderService self;
public void create() {
self.save(); // 走代理
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void save() { }
}
// 方案三:暴露代理,用 AopContext 取当前代理对象
// 需开启 @EnableAspectJAutoProxy(exposeProxy = true)
@Configuration
@EnableAspectJAutoProxy(exposeProxy = true)
public class AopConfig { }
public void create() {
((OrderService) AopContext.currentProxy()).save();
}
四、为什么 private / final / static 不行
- JDK 动态代理:基于接口实现,只能代理接口里声明的方法,实现类的额外方法完全不可见;
- CGLIB:生成子类并覆写方法,
private、final、static都无法覆写,final类直接报错; - 因此 Spring 的
@Transactional在 Spring 6 之前要求方法是public;6.0 起支持protected与包可见,但private仍然无效。
五、常见追问
- 问:怎么快速判断是不是代理问题? 打印
bean.getClass(),出现$Proxy或$$EnhancerBySpringCGLIB说明有代理;再在切面里打断点看是否进入。 - 问:
AopContext.currentProxy()为什么能拿到代理?exposeProxy=true时,代理在调用前把自身放入ThreadLocal,方法内通过它取回,本质是线程绑定的"当前代理"。 - 问:
@Async失效也是同理吗? 是,@Async同样靠代理,自调用、非 public 方法、同类调用都会失效。 - 问:切点表达式对
@Transactional也适用吗? 事务不是用户切面,它由BeanFactoryTransactionAttributeSourceAdvisor参与自动代理,但"是否被代理"的判定逻辑一致。
小结
记一句口诀:代理在,调用过代理,方法可覆写,注解能被看到——四条缺一条就失效。回答时先给根因,再摆表格,最后给出"拆类 / 自注入 / exposeProxy"三种修法,比罗列现象更有说服力。