“声明式事务”是 Spring 框架中最核心、也是日常开发中使用频率最高的特性之一。简单来说声明式事务就是“告诉框架我要做什么而不是具体怎么做”。它将事务管理的代码从业务逻辑中完全剥离出来开发者只需要通过注解如Transactional或XML 配置声明一下框架就会自动接管事务的开启、提交和回滚。为了让你在面试或实际应用中彻底讲透这个概念我将从对比、底层原理、失效场景高频考点三个维度进行深度解析。一、 为什么要用声明式事务对比编程式事务在 Spring 出现之前或者在 Spring 中使用编程式事务时代码是这样的// 编程式事务事务代码和业务代码严重耦合publicvoidcreateOrder(){TransactionStatusstatustransactionManager.getTransaction(configuration);try{// 1. 扣减库存inventoryService.deduct();// 2. 创建订单orderMapper.insert();transactionManager.commit(status);// 手动提交}catch(Exceptione){transactionManager.rollback(status);// 手动回滚throwe;}}痛点满屏幕的try-catch和commit/rollback业务逻辑被事务管理代码淹没代码侵入性极强且极易写错比如忘记 catch 某种异常导致没回滚。声明式事务的出现解决了这个问题// 声明式事务业务代码极其纯粹Transactional(rollbackForException.class)publicvoidcreateOrder(){inventoryService.deduct();orderMapper.insert();// 框架自动处理提交和回滚}核心思想基于AOP面向切面编程实现关注点分离。二、 底层原理Spring 是如何“自动”管理事务的这是面试中最喜欢深挖的点。声明式事务的本质是AOP 动态代理 事务拦截器。当你在方法上加上Transactional后Spring 在启动时会做以下事情解析注解Spring 容器启动时解析 Bean 上的Transactional注解提取事务属性如隔离级别、传播行为、回滚规则。创建代理对象Spring 发现这个 Bean 需要事务管理就会利用AOP为它生成一个代理对象如果目标类实现了接口默认用 JDK 动态代理否则用 CGLIB 代理。你从容器里拿到的其实是代理对象而不是原始对象。拦截器接管核心代理对象内部绑定了一个核心拦截器 ——TransactionInterceptor。执行流程当你调用代理对象的方法时请求会先到达TransactionInterceptor。拦截器获取PlatformTransactionManager事务管理器。开启事务调用getTransaction()获取数据库连接并开启事务。执行业务调用目标对象的真实方法proceed()。异常处理如果方法抛出异常拦截器捕获异常判断是否符合回滚规则。符合则调用rollback()否则调用commit()。一句话总结原理你调用的其实是代理对象的方法代理对象通过事务拦截器在执行业务代码前后自动帮你调用了Connection.commit()或Connection.rollback()。三、 实战与面试高频考点Transactional失效的 6 大场景在实际开发和面试中“声明式事务失效”是必考题。因为底层依赖 AOP 代理所以任何绕过代理对象的行为都会导致事务失效。1. 同类内部方法调用自调用问题——最常见坑ServicepublicclassOrderService{publicvoidmethodA(){this.methodB();// 直接调用同类方法绕过了代理}TransactionalpublicvoidmethodB(){// 这里的 Transactional 不会生效}}原因methodA调用methodB时使用的是this目标对象本身而不是 Spring 注入的代理对象。没有经过代理TransactionInterceptor就不会执行。解决办法将methodB抽离到另一个 Service 中推荐符合单一职责。注入自己Lazy Autowired private OrderService self;然后self.methodB()。使用AopContext.currentProxy()获取当前代理对象。2. 方法不是public的原因Spring AOP 默认只拦截public方法。如果加在protected、private或包可见的方法上注解直接失效Spring 4.0 会直接报错或忽略。解决老老实实加上public修饰符。3. 异常被catch吞掉了TransactionalpublicvoidcreateOrder(){try{orderMapper.insert();inti1/0;// 抛出异常}catch(Exceptione){log.error(出错了,e);// 异常被吃掉了没有抛给事务拦截器}}原因事务拦截器没有感知到异常认为方法正常执行完毕于是执行了commit()。解决在catch块中处理完日志后必须throw e或throw new RuntimeException(e)将异常抛出去。4. 抛出的异常类型不符合回滚规则原因SpringTransactional默认只对RuntimeException和Error进行回滚。如果你的代码抛出了受检异常如IOException、SQLException事务不会回滚而是会提交解决永远加上rollbackFor属性Transactional(rollbackFor Exception.class)。这是企业级开发的铁律。5. 数据库引擎本身不支持事务原因比如 MySQL 的表引擎是MyISAM不支持事务你加再多Transactional也没用。解决确保数据库表引擎为InnoDB。6. 传播行为Propagation配置错误原因如果配置了Transactional(propagation Propagation.NOT_SUPPORTED)意思是“以非事务方式执行如果当前存在事务则挂起”。这也会导致事务不生效。四、 进阶补充事务的传播行为Propagation当存在多个事务方法相互调用时Spring 如何决定事务的边界这就是传播行为。最常用的有三个REQUIRED默认如果当前有事务就加入如果没有就新建一个。95% 的场景用这个。REQUIRES_NEW无论当前有没有事务都挂起当前事务新建一个独立的事务。内层事务的回滚不会影响外层事务。常用于记录操作日志即使主业务回滚了日志也要保存下来。NESTED如果当前有事务则在嵌套事务内执行底层依赖数据库的Savepoint保存点机制。内层回滚外层也会回滚但外层回滚内层不一定回滚取决于具体实现。五、 总结声明式事务是 Spring 提供的极其优雅的解决方案它利用AOP 动态代理将事务管理从业务代码中解耦。但在享受便利的同时必须牢记它的边界和陷阱牢记“代理对象”的概念避免自调用失效。永远使用Transactional(rollbackFor Exception.class)。确保异常正确抛出不要被catch吞掉。在复杂的多数据源或嵌套调用场景下合理配置传播行为。