Spring 的 AOP 在什么场景下会失效?
一则或许对你有用的小广告
欢迎加入小哈的星球,你将获得:专属的实战项目(4个项目都能学) / 1v1 提问 / 简历修改 / Java 学习路线 / 社群讨论 / 学习打卡 / 每月赠书
《Spring AI 项目实战(问答机器人、RAG 智能客服、联网搜索)》已完结,基于
Spring AI + Spring Boot 3.x + JDK 21...,查看介绍《从零手撸:仿小红书(微服务架构)》 已完结,基于
Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...,查看介绍;演示链接:http://116.62.199.48:7070/《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接:http://116.62.199.48/
新开坑项目:《从零手撸:秒杀系统高并发优化实战》 正在更新中...,查看介绍
截止目前,星球内专栏累计输出 150w+ 字,讲解图 5110+ 张,还在持续爆肝中.. 后续还会上新更多项目,已有 4700+ 小伙伴加入学习,欢迎点击围观
面试考察点
-
代理原理理解:面试官不仅仅想知道你背了哪些失效场景,更想知道你是否理解 AOP 底层是基于动态代理实现的,代理对象和目标对象是两个不同的对象。
-
实战踩坑经验:考察你在实际项目中是否遇到过 AOP 失效的问题,能否快速定位原因。这块如果答不上来,面试官会觉得你实战经验不够。
-
Spring 体系认知:AOP 失效场景涉及 Bean 生命周期、事务管理、注解使用等多个方面,能系统性地回答,说明你对 Spring 整体有深入理解。
核心答案
Spring AOP 失效的常见场景有 6 大类:
| 序号 | 失效场景 | 核心原因 | 解决方案 |
|---|---|---|---|
| 1 | 同一个类内部方法调用 | this.method() 走的是原始对象而非代理对象 |
注入自身 / AopContext.currentProxy() |
| 2 | 方法非 public 修饰 |
Spring AOP 只能代理 public 方法 |
改为 public |
| 3 | 方法被 final/static 修饰 |
无法被重写或被子类覆盖 | 去掉 final/static |
| 4 | 目标对象未被 Spring 管理 | 不是 Spring Bean,无法生成代理对象 | 交给 Spring 容器管理 |
| 5 | 异常被 catch 吞掉 | 通知切不到异常,事务回滚失效 | 重新抛出或手动回滚 |
| 6 | @EnableAspectJAutoProxy 未开启 |
没有激活 AOP 功能 | 加上该注解 |
下面逐个展开讲。
深度解析
一、内部方法调用(最常考、最常踩坑)
这是面试官最爱问的,也是实际开发中最容易踩的坑。
@Service
public class UserService {
public void methodA() {
// 直接用 this 调用,走的是原始对象,不是代理对象!
this.methodB();
}
@Transactional
public void methodB() {
// 这里的事务注解会失效!
db.save(user);
}
}
为什么?来看一下调用链路:
上图展示了两种调用方式的本质区别:
- 外部调用:经过代理对象,增强逻辑(切面、事务等)正常执行,AOP 生效。
- 内部调用:
this.methodB()中的this指向的是目标对象本身,而不是代理对象。等于直接跳过了代理层,增强逻辑根本没机会执行。
这个坑我在实际项目中踩过不止一次,尤其是在事务场景下——methodA() 调 methodB(),结果 methodB() 上的 @Transactional 直接失效,数据写进去了一半报错,事务没回滚,搞得数据不一致。
解决方案有三种:
// 方案一:注入自身(推荐,最简洁)
@Service
public class UserService {
@Autowired
@Lazy // 解决循环依赖
private UserService self;
public void methodA() {
self.methodB(); // 通过代理对象调用
}
@Transactional
public void methodB() {
db.save(user);
}
}
// 方案二:使用 AopContext(需要在启动类加 exposeProxy)
@EnableAspectJAutoProxy(exposeProxy = true)
public class Application {}
@Service
public class UserService {
public void methodA() {
((UserService) AopContext.currentProxy()).methodB();
}
}
// 方案三:拆分到不同的 Service(结构最清晰)
@Service
public class UserService {
@Autowired
private OrderService orderService;
public void methodA() {
orderService.methodB(); // 跨 Bean 调用,天然走代理
}
}
二、方法修饰符问题
Spring AOP 有两种代理方式:
两种代理方式都 无法代理 private、static、final 方法,原因很简单:
private方法对外不可见,代理对象无法拦截static方法属于类级别,不参与多态final方法不能被子类重写,CGLIB 无法覆盖
@Service
public class UserService {
@Transactional
private void privateMethod() {
// ❌ 事务失效,private 方法无法被代理
}
@Transactional
public final void finalMethod() {
// ❌ 事务失效,final 方法无法被 CGLIB 重写
}
@Transactional
public static void staticMethod() {
// ❌ 事务失效,static 方法不参与代理
}
}
三、对象未被 Spring 容器管理
这个比较好理解。Spring AOP 的代理对象是在 Bean 初始化阶段生成的,如果你的类没有交给 Spring 管理(没有 @Component、@Service 等注解,也不是通过 @Bean 注册的),那 Spring 根本不知道这个类的存在,自然不会为它生成代理。
// ❌ 没有 @Service 注解,不是 Spring Bean
public class UserService {
@Transactional
public void save() {
// AOP 失效,Spring 不认识这个类
}
}
四、异常处理不当
这个场景通常出现在 @Transactional 结合 AOP 的情况下:
@Service
public class UserService {
@Transactional
public void save() {
try {
db.save(user);
int i = 1 / 0; // 抛出异常
} catch (Exception e) {
log.error("保存失败", e);
// ❌ 异常被吞掉了,事务通知切不到,不会回滚
}
}
}
事务通知是通过捕获方法抛出的异常来触发回滚的。你把异常 catch 了不抛出去,事务切面以为一切正常,就直接提交了——数据就脏了。
五、其他边缘场景
还有几个不太常见但值得知道的:
- 多线程场景:AOP 上下文是绑定在当前线程的(
ThreadLocal),如果在方法内部开了新线程,新线程里拿不到代理上下文 - 类加载器不一致:在不同类加载器下,代理类和目标类可能被认为不是同一个类型
- Spring Boot 2.x 之后默认使用 CGLIB:如果之前依赖 JDK 动态代理的代码,升级后行为可能变化
面试高频追问
-
JDK 动态代理和 CGLIB 的区别?哪个性能更好?
- JDK 基于接口,CGLIB 基于继承。JDK 7 之前 CGLIB 快,JDK 8 之后 JDK 动态代理被大量优化,性能差距已经很小了。Spring Boot 2.x 默认走 CGLIB。
-
@Transactional失效和 AOP 失效是一回事吗?- 本质上是一回事。
@Transactional就是通过 AOP 实现的,AOP 失效的场景,事务注解也会失效。但事务还有自己特有的失效场景,比如异常类型不对(默认只回滚RuntimeException,不回滚受检异常)。
- 本质上是一回事。
-
如何在项目中排查 AOP 失效问题?
- 第一,打个断点看调用栈,有没有经过代理类;第二,开启 Spring 的 debug 日志看代理创建过程;第三,直接
System.out.println(this.getClass())看输出的是不是代理类名。
- 第一,打个断点看调用栈,有没有经过代理类;第二,开启 Spring 的 debug 日志看代理创建过程;第三,直接
常见面试变体
- "Spring 事务在什么情况下会失效?"
- "为什么同一个 Service 内部方法调用事务不生效?"
- "JDK 动态代理和 CGLIB 有什么区别?Spring 默认用哪个?"
- "
@Transactional注解加在private方法上会怎样?"
记忆口诀
"内非私静未开启"——内部调用、非 public、final、static、未托管、未开启。六个失效场景记这六个字就行。
总结
AOP 失效的根因就一个:没有经过代理对象。不管是内部调用绕过代理、方法修饰符导致无法代理、还是对象压根不受 Spring 管理,殊途同归。面试中重点把 "内部方法调用" 这个场景讲透,再顺带提一下其他几个场景,基本就能拿满分。
