@Transactional加了怎么没回滚?→ 6种失效场景对照清单+同类内部调用不走代理的根因

发布时间:2026/7/29 16:38:40
@Transactional加了怎么没回滚?→ 6种失效场景对照清单+同类内部调用不走代理的根因 Spring 框架核心IoC 超级工厂 AOP 代理 Transactional 六种失效场景问题场景写了 Transactional抛了异常——数据没回滚。同一个类里调另一个 Transactional 方法事务没生效。盯着代码看了三遍没看出问题——因为问题不在代码逻辑在 Spring AOP 代理机制。30秒速览Transactional 失效的根因就一个——没走代理。6 种场景里 5 种本质都是Spring AOP 没拦截到你的方法调用。排查顺序先看是不是同类内部调用this.methodB() 绕过代理最常见→ 再看方法是不是 publicCGLIB 也拦不住非 public→ 最后看异常是不是被 catch 吞了。记住这条排查顺序比背 6 种场景高效得多。IoC 一句话别自己 new把创建谁、依赖谁交出去——BeanDefinition→反射创建→注入→单例池ConcurrentHashMap这条链走完你的对象才能被 Spring 管理。跟其他 Spring 文章不同这篇不从Spring 全家桶组件介绍开始——直接从 Transactional 失效这个最高频问题反推 IoC 和 AOP 的底层机制。搞清楚为什么同类内部调用不走代理this 指针指向原始对象而非代理对象你就彻底理解了 Spring AOP 的设计。本文是《Java 后端核心知识图谱》系列第 9 篇共 172 篇。一、IoC控制反转的本质IoC 到底是什么反转的是什么对象的创建和管理权从程序员 new反转给Spring 容器。传统方式UserService service new UserService(); // 自行创建对象并管理依赖 IoC 方式容器创建对象 → 容器注入依赖 → 容器管理生命周期容器内部结构Spring 启动时扫描配置 → 解析成BeanDefinitionBean 的配方类名、scope、lazy-init、依赖关系…通过BeanFactory按配方生产 Bean反射Class.newInstance()或 Constructor.newInstance()放入单例池本质是 ConcurrentHashMapgetBean() 时直接从 Map 中获取不是每次反射创建Bean 生命周期实例化 → 属性注入Autowired/Value→ Aware 回调 → BeanPostProcessor 前置处理 → PostConstruct → InitializingBean → BeanPostProcessor 后置处理 → 就绪 → PreDestroy → 销毁。三级缓存解决循环依赖A 依赖 BB 依赖 A。Spring 只解决单例 setter 注入的循环依赖singletonObjects一级缓存成品 Bean → earlySingletonObjects二级缓存提前暴露的半成品引用 → singletonFactories三级缓存ObjectFactory 能生成早期引用getSingleton()的查询逻辑DefaultSingletonBeanRegistry 源码① 先从 singletonObjects一级取 → 有则直接返回成品 Bean ② 没有 → 从 earlySingletonObjects二级取 → 有则返回半成品引用 ③ 还没有 → 从 singletonFactories三级取出 ObjectFactory → 调用 ObjectFactory.getObject() 生成早期引用 → 将结果升级到 earlySingletonObjects二级 → 从 singletonFactories 中删除该 ObjectFactory → 返回早期引用为什么必须用三级缓存ObjectFactory而非直接存早期引用关键在于 AOP 代理的时机。如果直接将半成品对象放入二级缓存此时对象尚未经过 BeanPostProcessor 后置处理——若该 Bean 需要 AOP 代理放入二级缓存的将是原始对象而非代理对象B 注入的 A 就不会是代理。三级缓存的ObjectFactory是一个() - getEarlyBeanReference(beanName, mbd, bean)的 lambda在getSingleton()被调用时才执行此时 SmartInstantiationAwareBeanPostProcessor 已经可以介入在暴露早期引用之前完成代理包装——保证 B 注入的 A 是代理。循环依赖的完整流程① A 实例化构造器 属性填充前 ② A 的 ObjectFactory 放入三级缓存 singletonFactories.put(A, () - getEarlyBeanReference(A, mbd, a)) ③ A 属性填充发现依赖 B → getBean(B) ④ B 尚未创建 → B 实例化 → B 的 ObjectFactory 放入三级缓存 ⑤ B 属性填充发现依赖 A → getBean(A) ⑥ getSingleton(A) 查询三级缓存 → 取出 ObjectFactory → 触发 getEarlyBeanReference如有 AOP 则在此生成代理 → 升级到二级缓存 → B 注入 A半成品引用 ⑦ B 完成属性填充 初始化 → 放入一级缓存 → B 创建完成 ⑧ A 拿到 B → 完成属性填充 初始化 → 放入一级缓存 → A 创建完成注意构造器注入的循环依赖无法解决——步骤①构造器阶段即需要依赖 B但此时 A 尚未实例化完毕无法放入三级缓存。需要先有对象才能暴露早期引用而构造器注入要求先有依赖才能完成实例化 → 死锁。三种注入方式方式特点建议构造器注入强制依赖不可为空、不可变final、测试友好推荐Spring 4.3 单构造器不用写 AutowiredSetter 注入可选依赖、可运行时改变可选依赖场景字段注入最简洁❌ 不推荐不能用 final、测试需反射注入、隐藏依赖Bean 作用域作用域含义适用场景singleton默认整个 IoC 容器中只有一个实例无状态 BeanService/DAOprototype每次 getBean() 都创建新实例有状态 Bean每次使用不同配置request每个 HTTP 请求一个实例Web 应用中请求级数据session每个 HTTP Session 一个实例用户登录信息⚠️prototype 的生命周期陷阱Spring 只管理 prototype Bean 的创建调用构造器注入依赖不管理完整的销毁生命周期。PreDestroy 不会自动回调——需要自行注册 DestructionAwareBeanPostProcessor 或手动调用。二、AOPJDK 动态代理 vs CGLIB核心概念链Joinpoint连接点→ Pointcut切入点表达式筛选要增强的方法→ Advice增强逻辑Before/After/Around→ Aspect切面 Pointcut Advice→ Weaving织入把切面应用到目标对象JDK 动态代理 vs CGLIBJDK 动态代理CGLIB原理基于接口Proxy.newProxyInstance()生成代理类实现接口基于继承生成目标类的子类重写方法要求必须有接口类和方法不能是 final生成对象$Proxy0类型 ≠ 目标对象类型注入时要用接口类型接收子类 instanceof 父类 ✓关系兄弟关系实现同一接口父子关系继承Spring Boot 策略演变Boot 1.x有接口用 JDK无接口用 CGLIBBoot 2.x统一默认 CGLIB不管有没有接口AOP 经典陷阱同类内部调用不走代理this.methodB()直接调用目标对象本身不经过代理 → Transactional/Cacheable 全部失效。// ❌ 同一个类中B 的 Transactional 不生效publicvoidA(){this.B();// this 是原始对象不是代理}TransactionalpublicvoidB(){...}// ✅ 解决注入自己通过代理调用AutowiredprivateXxxServiceself;publicvoidA(){self.B();// self 是代理走 AOP 拦截链}Async 异步处理Spring 的 Async 注解可以将方法调用异步化基于 AOP 代理实现AsyncpublicCompletableFutureStringprocessAsync(){// 此方法在独立线程池中执行returnCompletableFuture.completedFuture(done);}关键配置必须用EnableAsync开启默认使用SimpleAsyncTaskExecutor每次新建线程生产不可用生产必须自定义线程池配置ThreadPoolTaskExecutorcorePoolSize/maxPoolSize/queueCapacity并设置RejectedExecutionHandler推荐 CallerRunsPolicy——线程池满时退回调用线程同步执行自然限流与 Transactional 的交互Async 将方法提交到另一个线程执行——根据失效场景六多线程子线程拥有独立事务如果主线程和异步方法都需要事务各自独立管理一个失败不回滚另一个典型场景发邮件/短信通知非核心链路异步化不阻塞主流程写操作日志记录审计数据失败不影响业务调用金融系统铁律中的外部接口先提交本地事务再异步调用 ESB/RPC三、Transactional 六种失效场景Transactional 基于 AOP 实现Transactional 注解 → 生成代理对象 → TransactionInterceptor 拦截 → 开启事务setAutoCommit(false) 绑定 ThreadLocal→ 执行目标方法 → 提交/回滚。失效场景一同类内部调用最常见同一个类中 A 调 BB 上的 Transactional 不生效——因为this.B()不经过代理。失效场景二非 public 方法Transactional 默认只对 public 方法生效。private 方法 CGLIB 无法重写。失效场景三异常被 catch 吞掉自行 try-catch 了异常未向外抛出Spring 无法感知异常。如需回滚手动调用TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()。失效场景四rollbackFor 配错默认只回滚 RuntimeException 和 Error。Checked Exception如 IOException默认不回滚。需要Transactional(rollbackFor Exception.class)。失效场景五数据库引擎不支持事务MyISAM 引擎不支持事务。失效场景六多线程事务通过 ThreadLocal 绑定在当前线程上子线程无法继承父线程的事务上下文——这是 ThreadLocal 的固有特性与 Spring 无关。每个线程需要独立管理自己的事务各自使用Transactional注解事务之间完全隔离。将异步任务提交给线程池时子线程中的数据库操作处于独立的事务中失败不会回滚主线程的事务反之亦然。事务传播行为传播行为含义场景REQUIRED默认有则加入无则新建最常用共享一个事务REQUIRES_NEW挂起当前事务新建独立事务日志记录——日志失败不影响业务事务SUPPORTS有就用无就无事务查询方法NESTED嵌套事务保存点机制子事务回滚不影响外层金融系统铁律Transactional 方法里绝不包含外部调用ESB/RPC。外部调用可能超时 30s事务长时间持锁会把整个连接池拖垮。做法先提交本地事务再调外部接口外部失败走补偿逻辑。四、Spring Boot 自动配置核心注解 SpringBootApplication包含三个元注解SpringBootConfiguration即 Configuration标注配置类EnableAutoConfiguration自动配置的开关ComponentScan扫描同级包及子包下的 Component/Service/Controller自动配置原理① 加载 META-INF/spring/...AutoConfiguration.imports 文件3.x 用 importsspring.factories 已废弃 ② 里面 100 个自动配置类DataSourceAutoConfiguration、RedisAutoConfiguration... ③ 每个配置类上有条件注解 - ConditionalOnClassclasspath 有这个类才生效 - ConditionalOnMissingBean用户未自行定义 Bean 才采用默认配置 - EnableConfigurationProperties把 application.yml 绑定到 Properties 类一句话自动配置 条件装配 约定大于配置。Spring Boot 根据 classpath 中的 jar 自动推断所需配置不满意时自行定义 Bean 覆盖默认配置即可。五、SPI 机制Spring Boot 自动配置的基石SPI 是什么SPIService Provider Interface JDK 内置的服务发现机制。核心思想接口定义在 A 模块实现类在 B 模块——A 不知道 B 的存在但运行时通过 SPI 能加载 B 的实现。① 定义接口如 java.sql.Driver ② 提供方在 META-INF/services/接口全限定名 文件中写实现类的全限定名 ③ ServiceLoader.load(接口.class) 读取文件 → 反射实例化所有实现类 ④ 调用方拿到 Iterator遍历所有实现经典案例JDBC 驱动加载// 你只需写这一行DriverManager 内部通过 SPI 自动找到 MySQL Driver 并注册ConnectionconnDriverManager.getConnection(jdbc:mysql://...);// META-INF/services/java.sql.Driver 文件内容// com.mysql.cj.jdbc.Driver你从未手动Class.forName(com.mysql.cj.jdbc.Driver)但 DriverManager 找到了 MySQL 驱动——这就是 SPI。Spring Boot 自动配置本质 SPI 增强版Spring Boot 的META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports就是 SPI 文件——启动时读取所有 starter 的配置文件加载对应的自动配置类。与 JDK SPI 的核心差异JDK SPISpring Boot 自动配置加载策略加载所有实现按Conditional条件加载有对应 jar 才激活配置文件META-INF/services/接口名META-INF/spring/...AutoConfiguration.imports发现机制ServiceLoader.load()遍历Spring 容器启动时统一扫描一句话“SPI JDK 版的插件机制——接口和实现解耦实现类在运行时动态发现和加载。Spring Boot 的自动配置、JDBC 的驱动加载、SLF4J 的日志门面背后都是 SPI 的思想。”附Java 14 新特性速览——代码更少但语义更清晰金融系统 JDK 版本通常滞后多数项目仍用 JDK 8但理解语言演进方向有助于技术决策。以下聚焦四个关键新特性。RecordsJDK 16 正式不可变数据载体// 传统50 行 POJO字段 getter/setter equals hashCode toString// Record 一行搞定publicrecordUser(Longid,Stringname,Stringemail){}// 使用new User(1L, 张三, zhangexample.com)// getter 风格变为字段名本身user.id(), user.name()核心特性不可变final 字段无 setter→ 天然适合 DTO/VO/配置快照/返回值。限制不能继承类隐式 extends Record。Pattern Matching for instanceofJDK 16 正式// 传统判断 强转两步if(objinstanceofString){Strings(String)obj;...}// 新写法一步到位if(objinstanceofStringss.length()5){...}// s 可直接使用Switch 表达式JDK 14 正式// 箭头语法消除穿透 yield 返回值Stringresultswitch(dayOfWeek){caseMONDAY,TUESDAY,WEDNESDAY,THURSDAY,FRIDAY-工作日;caseSATURDAY,SUNDAY-休息日;};// 编译器检查是否覆盖全部枚举值// 复杂逻辑用 yieldStringmsgswitch(status){casePAID-已支付;caseCANCELLED-{log.info(订单取消);yield已取消;}default-未知;};Sealed ClassesJDK 17 正式 Text BlocksJDK 15 正式// Sealed Classes限制子类范围publicsealedclassPaymentpermitsAlipayPayment,WechatPayment,BankPayment{}// Text Blocks三引号多行字符串Stringjson { name: 张三, age: 30 } ;一句话“Java 新特性的方向不是增加复杂度而是减少样板代码的同时让语义更精确——Records 消灭 POJO 样板、Pattern Matching 消灭强转、Switch 表达式消灭穿透 bug、Sealed Classes 精确控制继承。金融项目保守是事实但语言演进的方向是明确的。”核心要点回顾IoC 容器的核心是将对象的创建和管理权从程序员 new反转给容器。容器内部的工作流程是扫描配置解析为 BeanDefinitionBean 的配方——类名、scope、lazy-init、依赖关系→ 通过反射创建实例 → 放入单例池本质为 ConcurrentHashMap。三级缓存是 Spring 解决循环依赖的关键设计singletonObjects一级成品 Bean→ earlySingletonObjects二级提前暴露的半成品引用→ singletonFactories三级ObjectFactory 能生成早期引用。A 依赖 B → B 依赖 A 的流程是A 实例化后暴露 ObjectFactory 到三级缓存 → 注入 B 时触发 B 的实例化 → B 注入 A 时从三级缓存获取 A 的早期引用 → B 创建完成 → A 拿到 B → A 创建完成。构造器注入无法解决循环依赖——构造器阶段对象尚未放入缓存需要先有依赖才能有对象形成死锁。AOP 两种代理有本质差异JDK 动态代理基于接口Proxy.newProxyInstance()代理类与目标类是兄弟关系——注入时需用接口类型接收vs CGLIB 基于继承生成目标类的子类父子关系类和方法不能是 final。Spring Boot 2.x 统一默认 CGLIB。同类内部调用不走代理是最常见的陷阱——this.methodB()直接调用目标对象本身不经过代理 → Transactional/Cacheable 全部失效解决方案是注入自身代理对象self.methodB()。Transactional 六种失效场景中同类内部调用加异常处理不当覆盖了 80% 的实际问题同类调用不经过代理、非 public 方法CGLIB 无法重写、异常被 catch 吞掉Spring 感知不到异常、rollbackFor 配错默认仅回滚 RuntimeException 和 Error、MyISAM 引擎不支持事务、多线程事务通过 ThreadLocal 绑定线程子线程无法继承。自动配置的本质是条件装配加约定大于配置——启动时加载自动配置类每个类通过 ConditionalOnClassclasspath 有对应 jar 才激活、ConditionalOnMissingBean用户未自行定义才采用默认、EnableConfigurationPropertiesapplication.yml 绑定到 Properties 类决定是否生效。其底层机制是SPI——JDK 内置的服务发现机制接口定义在 A 模块、实现在 B 模块运行时通过 ServiceLoader 反射实例化所有实现如 JDBC 驱动加载——从未Class.forName但 DriverManager 找到了 MySQL 驱动。Java 14 新特性Records/Pattern Matching/Switch 表达式/Sealed Classes/Text Blocks的方向是减少样板代码同时让语义更精确。上一篇《并发编程三》 |下一篇《消息队列RocketMQ金融级保障》系列专栏《Java 后端核心知识图谱》Java专栏聊聊你的经历Transactional 失效 6 种场景来对号入座你踩过几个① 用了 MyISAM ② 方法不是 public ③ 同类内部 this 调用 ④ catch 了异常没抛 ⑤ rollbackFor 忘设 ⑥ 传播行为配错。我先说③ 踩了不下 15 次④ 踩了 5 次——你觉得哪个最坑评论区留下你踩坑最多的编号 如果这张对照清单帮你下次少 debug 半天欢迎收藏点赞

相关新闻