🕐 线上事故 — 周五 23:47
🚨 你加了 @Transactional,但异常时数据还是提交了
你写了一个订单创建方法,加了 @Transactional,测试环境一切正常。
上线后,用户反馈"下单失败但扣了钱"——异常抛出了,数据库却没有回滚。
你百思不得其解:明明加了 @Transactional,为什么事务没生效?
🔍 排查发现:你的 batchCreate() 方法内部调用了 this.create()
💥 this 是原始对象,不是代理对象 → 绕过了代理 → 事务增强根本没执行
💡 Spring 对你隐瞒了什么?它把你的 Service 替换成了一个"替身"(代理对象),
但你在类内部用 this 调用时,走的是原始对象,替身根本没机会介入。
👉 本页核心问题:Spring 运行时到底做了什么?你的代码真的在按你以为的方式执行吗?
Spring 运行真相
7大核心主题 · 6个SVG · 8个IDE代码实战 · 8道面试FAQ · 5种❌✅对比
Bean 生命周期
AOP 代理原理
@Transactional 真相
CGLIB vs JDK
代理失效5大坑
三层结构:基础认知 → 实战应用 → 总结速查。★ 标记的是核心/面试高频章节
① Bean 从无到有:Spring 启动时做了什么
你以为 @Service 只是标记?启动时它经历了一条完整的流水线
Spring 启动时对每个 Bean 走这条流水线,AOP 代理在步骤5 中悄悄替换了原始对象
🏭 类比:工厂流水线生产一个产品
📐 BeanDefinition = 产品图纸(描述要生产什么、有什么参数)
🔨 实例化 = 把原材料铸成毛坯(壳子有了,但零件还没装)
🔩 属性注入 = 装零件(把依赖的螺丝/电路板装上去)
📋 Aware 回调 = 贴产品编号(告诉它自己在哪条产线)
🎨 BeanPostProcessor = 喷漆/贴标/质检(这里是 AOP 代理"包装"产品的环节)
✅ 放入仓库 = 质检合格的成品入库,随时发货
关键认知
Spring 启动时就把所有 Bean 创建完毕并放好了。运行时只是从容器里拿现成的。这和你写的 new 完全不同——Spring 帮你 new,帮你注入依赖,帮你创建代理,最后给你一个"成品"。这就是 IoC(控制反转):对象的创建权从你的代码转移给了 Spring。
👆 上面看到 Bean 创建有6个步骤,其中步骤2和步骤5最容易搞混。下面我们拆开看——实例化和初始化到底什么区别?为什么90%的新手在这个地方踩坑?
② 实例化 ≠ 初始化:这两个概念必须区分
90% 的困惑来自把"实例化"和"初始化"混为一谈。最直接的后果:构造器里用 @Autowired 字段 → NPE
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderMapper orderMapper; // 步骤3注入
@Value("${app.name}")
private String appName; // 步骤3注入
// ====== 步骤2:实例化 ======
// JVM 执行:new OrderServiceImpl()
// 此时:orderMapper = null, appName = null
// 对象已经存在,但内部是"空壳"
@PostConstruct
public void init() {
// ====== 步骤5:初始化 ======
// 此时 orderMapper 已经注入好了,可以安全使用
System.out.println(appName + " 启动完成");
// ✅ 安全:orderMapper 不为 null
}
}
为什么区分这么重要?
| 阶段 | 做了什么 | 此时 @Autowired 字段 | 能安全使用吗 |
| 实例化 | new 构造方法 | ❌ null | ❌ 构造器中不能用 |
| 属性注入 | Spring 反射注入 | ✅ 已注入 | ⚠️ 但实例不完整 |
| 初始化 | @PostConstruct | ✅ 可用 | ✅ 完全安全 |
@Service
public class BadOrderService {
@Autowired
private OrderMapper orderMapper;
public BadOrderService() {
// ❌ 构造器执行时机 = 步骤2(实例化)
// 此时 Spring 还没执行步骤3(属性注入)!
// orderMapper 是 null!
orderMapper.selectAll(); // 💥 NullPointerException!
}
}
// ✅ 方式1:@PostConstruct(最常用)
@Service
public class GoodOrderService {
@Autowired
private OrderMapper orderMapper;
@PostConstruct // ← 步骤5才执行,此时orderMapper已注入
public void init() {
orderMapper.selectAll(); // ✅ 安全!orderMapper 不为 null
}
}
// ✅ 方式2:构造器注入(Spring推荐的写法)
@Service
public class BetterOrderService {
private final OrderMapper orderMapper;
@Autowired // ← Spring 先创建依赖,再调构造器
public BetterOrderService(OrderMapper orderMapper) {
this.orderMapper = orderMapper; // ✅ 构造器注入,不为 null
orderMapper.selectAll(); // ✅ 安全!
}
}
判断口诀
构造器中用字段 → 问自己:此时字段注入了吗?如果是 @Autowired 字段注入 → 还没注入(步骤2先于步骤3)→ ❌ NPE。如果是构造器参数注入 → Spring 保证先创建依赖再调构造器 → ✅ 安全。
👆 上面看到 Bean 创建有6步,其中步骤5的 BeanPostProcessor 有一项秘密任务——AOP 代理创建。下面我们就揭开这个替身的真面目:你以为你在调原始对象,其实你在调一个"替身"。
③ AOP 代理到底是什么
这是 Spring "魔法"的核心——你以为你在调原始对象,其实你在调替身
AOP 代理像一个"洋葱"——你的代码在核心,外面裹着日志、事务、权限等增强层
🎬 类比:电影里的替身演员
🎥 导演(Controller)说"开拍!"——他以为自己对着明星本人喊的。
🎭 但实际走出来的可能是替身(代理对象)。替身做了大部分准备工作,只有危险动作的瞬间才让真正的明星(你的业务代码)上场。
🎬 替身(代理)做什么:
① 开拍前化妆、道具准备(@Before / 开启事务)
② 明星本人表演核心镜头(你的 Service 代码)
③ 收工后场务清理、结算(@After / 提交事务)
导演完全无感——他以为自己一直在跟明星说话。Controller 也是——它以为自己调的是原始 Service。
调用时从外到内(日志→事务→业务),返回时从内到外(业务→事务→日志)。这就是"洋葱模型"
👆 现在你知道 AOP 代理是"替身"了。但这个替身是怎么被创造出来的?CGLIB 用字节码技术偷偷生成了你的类的子类。下面看源码级拆解。
④ CGLIB 代理是怎么创建的
Spring 在启动时(步骤5)偷偷用字节码技术生成了一个子类
// ========== 你写的原始类 ==========
@Service
@Transactional
public class OrderServiceImpl implements OrderService {
public Order create(OrderDTO dto) {
// 业务代码
return order;
}
}
// ========== Spring 用 CGLIB 生成的子类(运行时动态创建,你看不到)==========
// CGLIB 通过 ASM 字节码操作,在内存中生成了类似下面的类:
public class OrderServiceImpl$$EnhancerByCGLIB$$abc1234
extends OrderServiceImpl { // ← 继承你的类!
private MethodInterceptor interceptor; // 拦截器链
@Override
public Order create(OrderDTO dto) {
// 这就是代理逻辑!先增强,再调父类(即原始代码)
return interceptor.intercept(
this, // 代理对象
OrderServiceImpl.class.getMethod("create"), // 目标方法
new Object[]{dto}, // 参数
methodProxy // 调用父类原始方法的代理
);
// interceptor.intercept 内部逻辑:
// 1. 执行 @Before(日志等)
// 2. 开启事务(getConn + setAutoCommit(false))
// 3. super.create(dto) ← 调用父类,即你写的真正代码
// 4. commit() 或 rollback()
// 5. 执行 @After
// 6. 返回结果
}
}
// ========== Controller 注入的是谁?==========
@Autowired
private OrderService orderService;
// ↑ 实际注入的是 OrderServiceImpl$$EnhancerByCGLIB$$abc1234
// ↑ 不是原始的 OrderServiceImpl!
CGLIB vs JDK 动态代理
| 对比项 | JDK 动态代理 | CGLIB 代理(Spring 默认) |
| 原理 | 基于接口,生成实现类 | 基于继承,生成子类 |
| 要求 | 目标类必须实现接口 | 不需要接口,但不能是 final 类 |
| 代理对象类型 | 接口的实现类 | 目标类的子类 |
| Spring Boot 2.x+ | 默认不使用 | ★ 默认使用 |
| 性能 | 创建快,调用稍慢 | 创建稍慢,调用快 |
| final 方法 | 接口中的 default 可代理 | ❌ final 方法不能被重写→不能代理 |
💡 Spring Boot 2.x 之后默认用 CGLIB
spring.aop.proxy-target-class=true(默认值)。即使你的类实现了接口,也用 CGLIB(生成子类)。所以你经常会看到 class 名带 $$EnhancerByCGLIB$$ 后缀。
👆 代理原理搞清楚了。现在最关键的问题来了:既然代理对象帮你开启事务,那什么情况下事务会"不生效"?答案是:凡是绕过代理的调用,事务都不生效。下面5大坑,面试必考。
⑤ @Transactional 失效的5大经典场景(❌✅代码对照)
一旦理解了代理原理,这些"坑"就不再是死记硬背——所有失效的根因都一样:请求没有经过代理对象
坑1:类内部方法调用(最常见的坑,占失效案例60%+)
失效原因:内部调用走的是 this.method(),而 this 是原始对象,不是代理对象 → 绕过了代理 → 事务增强不生效。
@Service
public class OrderServiceImpl {
@Autowired
private OrderService self; // ← 注入自己(代理对象)
public void batchCreate(List<OrderDTO> list) {
for (OrderDTO dto : list) {
// ❌ 错误:this.create() 走的是原始对象,事务不生效
// this.create(dto);
// ✅ 正确:通过 self(代理对象)调用,事务生效
self.create(dto);
}
}
@Transactional
public Order create(OrderDTO dto) {
return orderMapper.insert(dto);
}
}
坑2:@Transactional 加在 private/final/static 方法上
失效原因:CGLIB 代理通过生成子类 + 重写方法实现。private 方法子类看不到,final 方法不能重写,static 方法属于类不属于实例 → 代理对象拦截不到 → 事务不生效。
@Service
public class TxMethodDemo {
@Transactional
private void privateCreate() { // ❌ private: CGLIB子类看不到这个方法
orderMapper.insert(order); // → 代理拦截不到 → 事务不生效
}
@Transactional
public final void finalCreate() { // ❌ final: CGLIB子类不能重写
orderMapper.insert(order); // → 代理拦截不到 → 事务不生效
}
@Transactional
public static void staticCreate() { // ❌ static: 不属于实例,代理管不到
orderMapper.insert(order); // → 代理拦截不到 → 事务不生效
}
@Transactional
public void publicCreate() { // ✅ public: CGLIB子类可以重写
orderMapper.insert(order); // → 代理正常拦截 → 事务生效
}
}
坑3:异常被 catch 吃掉了
失效原因:事务回滚的触发条件是代理对象检测到异常从方法中抛出。如果你在方法内部 try-catch 了异常,代理对象看不到异常 → 认为执行成功 → commit 而不是 rollback。
@Transactional
public void createOrder(OrderDTO dto) {
try {
orderMapper.insert(order);
detailMapper.insert(detail);
} catch (Exception e) {
log.error("创建失败", e);
// ❌ 异常被catch吞掉了!
// 代理看不到异常 → commit → 数据已入库,无法回滚!
}
}
// ✅ 修复方式1:re-throw
@Transactional(rollbackFor = Exception.class)
public void createOrderGood1(OrderDTO dto) {
try {
orderMapper.insert(order);
detailMapper.insert(detail);
} catch (Exception e) {
log.error("创建失败", e);
throw new RuntimeException(e); // ✅ 重新抛出,代理能检测到
}
}
// ✅ 修复方式2:手动标记回滚
@Transactional
public void createOrderGood2(OrderDTO dto) {
try {
orderMapper.insert(order);
detailMapper.insert(detail);
} catch (Exception e) {
log.error("创建失败", e);
TransactionAspectSupport.currentTransactionStatus()
.setRollbackOnly(); // ✅ 手动标记回滚
}
}
坑4:新线程丢失事务上下文
失效原因:Spring 事务基于 ThreadLocal 绑定 Connection。新线程有新的 ThreadLocal → 拿不到当前事务的 Connection → 新线程的数据库操作不受事务管控。
@Transactional
public void createOrder(OrderDTO dto) {
orderMapper.insert(order); // 主线程:有事务
// ❌ 新线程:ThreadLocal 是空的 → 不在事务中
CompletableFuture.runAsync(() -> {
logMapper.insert(log); // 💥 不受事务管控!
});
}
// ✅ 修复:在新线程中手动开启编程式事务
@Autowired
private PlatformTransactionManager txManager;
public void createOrderGood(OrderDTO dto) {
orderMapper.insert(order);
CompletableFuture.runAsync(() -> {
TransactionTemplate txTemplate = new TransactionTemplate(txManager);
txTemplate.execute(status -> { // ✅ 新线程独立事务
logMapper.insert(log);
return null;
});
});
}
坑5:checked 异常默认不回滚
失效原因:Spring 默认只对 RuntimeException 和 Error 回滚。如果你抛出的是 checked exception(如 IOException),默认不回滚。
@Transactional
public void uploadAndSave() throws IOException {
orderMapper.insert(order);
fileService.upload(); // 抛出 IOException (checked)
// ❌ IOException 是 checked exception
// Spring 默认不回滚 → order 已经入库了!
}
// ✅ 修复:显式指定 rollbackFor
@Transactional(rollbackFor = Exception.class) // ← 加上这个!
public void uploadAndSaveGood() throws IOException {
orderMapper.insert(order);
fileService.upload(); // IOException → ✅ 回滚!
}
🔴 @Transactional 失效速查表
| 坑 | ❌ 错误写法 | ✅ 正确写法 | 根因 |
| 1. 内部调用 | this.create() | self.create() / AopContext | 绕过代理 |
| 2. 方法修饰符 | private/final/static | public | 代理无法重写 |
| 3. 异常吞掉 | catch 后不 re-throw | throw / setRollbackOnly | 代理看不到异常 |
| 4. 新线程 | CompletableFuture | TransactionTemplate | ThreadLocal 隔离 |
| 5. checked 异常 | @Transactional (无参) | rollbackFor=Exception.class | 默认只回滚 RuntimeException |
核心逻辑
所有 @Transactional 失效的根因都一样:请求没有经过代理对象。记住这个判断链:@Transactional 是否生效 → 取决于调用是否经过代理 → 取决于调用方式是否通过 Spring 注入的 Bean → 类内部 this.method() 不经过代理 → 失效。理解了代理原理,这5个坑不需要死记。
👆 5大坑搞清楚了。现在我们把前面的所有概念串成一条完整的执行链——从 Controller 收到请求到数据库写入再回来,看看代理、ThreadLocal、连接池是怎么协作的。
⑥ 完整请求链路:从 Controller 到 DB 再回来
把前面的所有概念串成一条完整的执行链
① Controller 接收请求
Tomcat 线程执行 OrderController.createOrder()
Controller 中注入的 orderService 实际是 代理对象
② 代理前置
进入代理对象的 create() → CGLIB 拦截器介入
执行 @Before 通知 → 开启事务:从连接池获取 Connection → setAutoCommit(false)
关键:Connection 绑定到 ThreadLocal,同一线程内后续的 Mapper 调用共用这个连接
③ 调用原始方法
代理对象通过 methodProxy.invokeSuper() 调用父类(原始 OrderServiceImpl)
你的业务代码开始执行:new Order() → set 属性 → orderMapper.insert()
④ Mapper 执行
OrderMapper 也是代理对象(MyBatis 用 JDK 动态代理)
MyBatis 代理:找 SQL → 参数绑定 → 从 ThreadLocal 获取 Connection → JDBC execute → MySQL
注意:Mapper 和 Service 用的是同一个 Connection(通过 ThreadLocal 传递),这就是事务能保证原子性的原因
⑤ 返回原始方法
Mapper 返回 → Service 返回 Order → 回到代理对象
⑥ 代理后置
无异常 → connection.commit() → 事务提交
有异常 → connection.rollback() → 事务回滚
执行 @After 通知 → Connection 归还连接池 → ThreadLocal 清理
⑦ 返回 Controller
代理返回 Order → Controller 包装成 Result → Jackson 序列化 → 响应浏览器
事务的本质:代理负责开关事务,ThreadLocal 保证同线程共享连接,连接池管理连接生命周期
🔑 事务的本质
Spring 事务 = 代理 + ThreadLocal + 连接池。代理负责在方法前后开关事务,ThreadLocal 保证同一线程内共用一个 Connection,连接池负责管理连接。三者缺一不可。
这就是为什么 @Transactional 只在通过代理调用时生效——代理是事务的发起者,绕过代理就没有人帮你 commit/rollback 了。
👆 完整链路串完了!最后来高频疑问速查——点击展开查看,面试前快速过一遍。
⑦ 高频疑问(点击展开)
Q1:Bean 是单例的,200 个线程同时调用同一个 Service 会冲突吗?
不会冲突,前提是你不使用成员变量存请求数据。因为虽然 Service 是单例(堆中一个对象),但每个线程执行时的局部变量在各自的栈上——线程私有。
Thread-1 的 create() 栈帧里的 order 变量和 Thread-2 的完全隔离。只要你的 Service 没有可变的成员变量(只有无状态方法),它就是天然线程安全的。
危险写法:在 Service 中加 private int count = 0; 然后在方法中 count++ → 200 个线程同时修改堆中同一个 count → 并发问题。
Q2:Spring 启动很慢,Bean 创建做了什么?
启动时要对每个 Bean 走完整生命周期:实例化 → 注入依赖 → 初始化。如果项目有几千个 Bean,每个 Bean 还要依赖其他 Bean(有依赖链),整个过程可能需要十几秒到几十秒。
这就是为什么 Spring Boot 2.2+ 引入了懒加载(spring.main.lazy-initialization=true)——启动时不创建 Bean,第一次被使用时才创建。但生产环境一般不用,因为第一个请求会变慢。
Q3:循环依赖是什么?Spring 怎么解决?
A 依赖 B,B 又依赖 A → 创建 A 时需要 B,创建 B 时又需要 A → 死循环。
Spring 用三级缓存解决:创建 A 时先把 A 的"半成品"(刚实例化还没注入依赖)放进缓存 → 去创建 B → B 注入时从缓存拿到 A 的半成品 → B 创建完成 → 回来把 B 注入到 A → A 完成。
限制:构造器循环依赖无法解决(因为构造器阶段还没法把半成品放进缓存)。只有 setter/字段注入的循环依赖才能自动解决。
Q4:拦截器(Interceptor)、过滤器(Filter)、AOP 有什么区别?
执行顺序:Filter → Interceptor → Controller → Interceptor → Filter
Filter:Servlet 规范级别,在请求到达 Spring 之前就执行。可以修改 request/response。常用于:编码、CORS、日志。
Interceptor:Spring MVC 级别,在 DispatcherServlet 路由前后执行。能拿到 HandlerExecutionChain。常用于:鉴权、限流。
AOP:Bean 级别,通过代理拦截方法调用。粒度最细(到具体方法)。常用于:事务、日志、缓存。
它们是不同层级的拦截机制,可以同时使用。
Q5:为什么 Spring 选择 CGLIB 而不是 JDK 动态代理?
JDK 动态代理要求目标类必须实现接口。但很多 Service 类只有一个接口,或者根本没接口(直接是类)。JDK 代理无法覆盖。
CGLIB 通过生成子类实现代理,不需要接口,覆盖面更广。Spring Boot 2.x 开始默认 proxy-target-class=true,统一用 CGLIB,避免"有时候生效有时候不生效"的困惑。
限制:CGLIB 不能代理 final 类和 final 方法(不能继承/重写)。所以你的 Service 类不要加 final。
Q6:@Transactional(propagation = ...) 有几种传播行为?什么时候用?
7种传播行为,最常用的3种:
• REQUIRED(默认):当前有事务就加入,没有就新建。95%的场景用这个。
• REQUIRES_NEW:总是新建事务,挂起当前事务。用于:独立操作(如日志),不受外层事务回滚影响。
• NESTED:在当前事务中创建保存点(savepoint),回滚只回到保存点。用于:部分失败不影响整体。
其余4种(SUPPORTS/NOT_SUPPORTED/MANDATORY/NEVER)极少使用,面试说出上面3种就够了。
Q7:怎么确认我的 Bean 到底是不是代理对象?
在代码中打印 orderService.getClass().getName():
• 如果类名带 $$EnhancerByCGLIB$$ → CGLIB 代理对象 ✅
• 如果类名带 $Proxy → JDK 动态代理对象 ✅
• 如果类名就是原始类名 → 没有被代理(没有 AOP 增强)
也可以在 IDEA 调试时查看 Variables 面板,代理对象会显示特殊类型。
Q8:@Async 和 @Transactional 能一起用吗?
可以一起用,但要注意顺序:
• @Async @Transactional → 先创建事务代理,再创建异步代理。异步线程中事务正常工作。
• 如果只在方法上标了 @Async,异步线程中的数据库操作没有事务(新线程的 ThreadLocal 是空的)。
• 如果异步方法需要事务,必须同时标注 @Transactional,Spring 会在异步线程中独立开启事务。
🎯 心智模型总结
Spring 的"魔法"本质上只有两件事:IoC(帮你创建和管理对象)和 AOP(帮你在方法前后增强逻辑)。理解了 Bean 生命周期和代理原理,你就理解了 Spring 90% 的行为。
下次遇到"为什么不生效",问自己:这次调用经过了代理对象吗? 如果没有,那就是答案。