主题
Spring 生产问题排查:事务失效、循环依赖与源码定位
本文是 Spring 系统学习系列的 L3 实战篇。前置:Bean 生命周期扩展点、AOP 与事务源码。 学完可以配合面试题食用:19. Spring 生产问题排查:配置加载顺序与覆盖
先建立一个判断框架:Bean 不是你以为的那个 Bean
前面两篇讲容器和 AOP 的源码,这篇解决一个更现实的问题:代码在测试环境好好的,上了生产就事务失效、启动报循环依赖、Bean 莫名其妙被覆盖。翻 Spring 的源码不是为了背流程,是为了在这种时刻知道往哪下断点。
先给一个可以背下来的判断框架:Spring 生产问题十有八九是"Bean 不是你以为的那个 Bean"。具体拆开是三种形态:
- 代理不一致:你拿到的引用是代理对象,调用路径却绕过了代理。事务失效八成是这个。
- 条件装配不符预期:你以为生效的配置类没生效,或被另一个 Bean 覆盖。自动装配排错的核心。
- 上下文分裂: MVC 的
ApplicationContext和 Root Context 是两个容器,组件注册进了错误的那个,AOP 自然切不到。
后面每一节都是在这个框架下展开:先讲排查路径,再给工具和代码。
事务失效:三步排查路径
事务失效是线上最常见的一类问题。回忆 31 篇的内容:@Transactional 靠 TransactionInterceptor 环绕 Bean 的代理实现,所以失效的根源几乎都能归结为"调用没走代理"。排查按固定三步走:
mermaid
flowchart TD
A[事务失效报告] --> B["1. 确认调用是否经过代理<br/>(this 调用 / new 出来的对象 / private 方法)"]
B -->|绕过代理| C[改为注入自身代理或拆方法]
B -->|走了代理| D["2. 开 SQL 日志确认语句<br/>(MySQL: general_log / log4jdbc)"]
D -->|SQL 未执行或异常类型不对| E[检查异常类型与 rollbackFor、传播行为]
D -->|SQL 正常但没提交| F["3. 开事务同步器日志<br/>isActualTransactionActive 打点"]
F --> G[定位: 事务被谁提前提交 / 连接池配置 / 多数据源]第一步确认代理。同一个类里 methodA() 直接调 this.methodB(),methodB 上的 @Transactional 不生效,因为 this 是原始对象不是代理。同理,手动 new 出来的对象、private 方法、final 方法,全都不走代理。
第二步看 SQL 日志。配置 logging.level.org.hibernate.SQL=debug 或直接开 MySQL 的 general_log,确认语句到底执行没执行、连接有没有换。如果每次 SQL 拿到的是不同的 Connection,说明事务边界已经乱了(常见于多数据源没配对 TransactionManager)。
第三步上打点法,这是本篇最重要的实操代码:
java
@Component
public class TxProbe implements TransactionSynchronization {
// 在任意被怀疑的方法里调用,打出一行事务状态
public static void dump(String where) {
boolean active = TransactionSynchronizationManager.isActualTransactionActive();
boolean readOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly();
String txName = TransactionSynchronizationManager.getCurrentTransactionName();
log.info("[TX-PROBE] at={} active={} readOnly={} txName={}",
where, active, readOnly, txName);
if (active) {
TransactionSynchronizationManager.getSynchronizations()
.forEach(s -> log.info("[TX-PROBE] sync={}", s.getClass().getName()));
}
}
}在被怀疑的方法第一行调用 TxProbe.dump("OrderService.createOrder"),日志会告诉你三件事:这里到底有没有活的事务、是不是只读、事务名是不是你期待的那个方法。配合下面的日志片段对照看:
text
# 正常:代理生效,事务包住了方法
[TX-PROBE] at=OrderService.createOrder active=true readOnly=false txName=com.demo.OrderService.createOrder
o.s.j.d.DataSourceTransactionManager : Initiating transaction commit
# 异常:this 调用绕过了代理
[TX-PROBE] at=OrderService.createOrder active=false readOnly=false txName=null第二段日志里 active=false,说明方法压根没在事务里跑,回去查调用路径,十有八九是自调用。
循环依赖:为什么 Field 注入没事,构造器注入就炸
03 篇讲过三级缓存的原理,这里补上排查视角。现象很典型:把 @Autowired 字段注入改成构造器注入后,启动直接报错:
text
The dependencies of some of the beans in the application context form a cycle:
┌─────┐
| aService defined in file [AService.class]
↑ ↓
| bService defined in file [BService.class]
└─────┘根因在于两种注入方式对"对象何时可用"的要求不同。字段注入时,容器可以先把半成品的 A(提前暴露引用)放进三级缓存,B 拿到这个引用先完成初始化,回头 A 再注入 B,循环就解开了。构造器注入要求实例化时依赖必须全部就位,A 等着 B、B 等着 A,谁也迈不出第一步,只能报错。
mermaid
sequenceDiagram
participant C as 容器
participant A as AService
participant B as BService
Note over C,B: 字段注入(可解)
C->>A: 实例化 A(半成品,提前暴露引用)
C->>B: 实例化 B
B->>A: 注入半成品 A 的引用
C->>B: B 完成初始化
C->>A: A 注入完成的 B,A 完成初始化
Note over C,B: 构造器注入(死锁)
C->>A: new AService(b) 需要 B
C->>B: new BService(a) 需要 A
A--xC: 等待 B,B 等待 A,抛 BeanCurrentlyInCreationExceptionspring.main.allow-circular-references=true 能让构造器循环依赖在某些版本下放行,但别在生产上开。它会放宽 Spring Boot 2.6 起的默认禁止策略,把设计问题藏起来——今天能启动,明天加一个 Bean 就可能在运行期拿到未完成初始化的对象。正确的做法是拆循环:抽出第三个 Bean 承载公共逻辑,或者用 @Lazy 延迟一边的注入。
Bean 重复与覆盖:@ConditionalOnMissingBean 为什么没保住你
自动装配类里常见这样的代码:
java
@Bean
@ConditionalOnMissingBean(ObjectMapper.class)
public ObjectMapper objectMapper() { ... }意图是"用户自己定义了就用用户的"。线上翻车场景是:用户的 ObjectMapper 没生效,框架的默认实现赢了。两个高频原因:
原因一:包扫描把配置类漏了。 @ConditionalOnMissingBean 判断的是 Bean 定义注册的时序——自动配置在用户配置之后处理才成立。如果用户的配置类不在 @ComponentScan 范围内,根本没注册进来,条件判断自然"通过",默认 Bean 上位。这时要用 ConditionalEvaluationReport 看现场(下一节)。
原因二:同名 Bean 静默覆盖。 两个配置类定义了同名 Bean(都叫 dataSource),Spring Boot 2.6 之前默认允许覆盖(spring.main.allow-bean-definition-overriding=true 是老默认值),后注册的悄悄干掉先注册的,没有任何报错。升级到 2.6+ 后默认改为禁止,启动直接报 BeanDefinitionOverrideException——很多团队升级时被这个报错救了一命,才发现线上早就在用被覆盖的错误配置。如果确认要覆盖,显式配置:
yaml
spring:
main:
allow-bean-definition-overriding: true # 只在明确知道谁覆盖谁时才开排查这类问题的固定动作:把启动日志开到 debug,看每个 Bean 定义来自哪个 ConfigurationClass:
bash
java -jar app.jar --debug--debug 会在启动失败时打出 CONDITIONS EVALUATION REPORT,哪些配置类匹配、哪些没匹配、为什么没匹配,一目了然。下一节展开。
源码定位技巧:三件武器
前面几节反复出现"去源码里确认",这里给三个具体技巧,都是从实践里攒出来的。
技巧一:条件断点锁 BeanName。 在 AbstractAutowireCapableBeanFactory#doGetBean 或 DefaultSingletonBeanRegistry#getSingleton 打断点,条件设为 beanName.equals("orderService"),避免在几千个 Bean 的创建流程里一次次手动放行。IDEA 里在断点上右键填 Condition 即可。这是追"Bean 到底是谁创建的、被谁覆盖的"最快的路。
技巧二:ConditionalEvaluationReport。 除了启动失败时自动打印,还能主动把它导出来做体检:
java
// 任意@Configuration类里注入报告,启动完成后打印
@Bean
public ApplicationRunner reportDumper(ConditionalEvaluationReport report) { ... }
// 更简单的方式:启动时加 --debug 参数
// java -jar app.jar --debug报告长这样,Matched/Did-not-match 两栏:
text
Positive matches:
-----------------
DataSourceAutoConfiguration matched:
@ConditionalOnClass found required classes 'javax.sql.DataSource';
Negative matches:
-----------------
RedisAutoConfiguration:
Did not match:
@ConditionalOnClass did not find required class 'redis.clients.jedis.Jedis'RedisAutoConfiguration 没生效的原因(缺 Jedis 类)直接写在报告里,不用猜。
技巧三:Spring Boot FailureAnalyzer。 启动失败时那一小段人话提示(比如循环依赖的那个方框图)就是 FailureAnalyzer 干的。排查时别只看这段提示,把它当索引用:提示里给的类名进 FailureAnalyzer 的实现类(如 BeanCurrentlyInCreationFailureAnalyzer),看它从异常里提取了哪些信息、对应源码哪一段,顺着就能找到根因判断逻辑。
常见误区与小结
- 误区一:
@Transactional加了就万事大吉。 自调用、异常被 catch 吞掉、异常类型不匹配 rollbackFor,任何一条都能让回滚失效;先打isActualTransactionActive再说话。 - 误区二:把
allow-circular-references=true当修复。 它只是把报错推迟到更难复现的时刻,循环依赖该拆就拆。 - 误区三:看到
BeanDefinitionOverrideException就把它关掉。 这个报错是帮你的,先搞清谁覆盖了谁,再决定显式允许还是修配置。 - 误区四:debug 级日志只在出事时开。 CONDITIONS EVALUATION REPORT 在启动失败时才打印,平时用
--debug定期体检一次,能提前发现自动装配的漂移。
小结一下:Spring 的生产问题排查,框架就一句话——先确认 Bean 是不是你以为的那个 Bean。事务失效查代理路径,循环依赖看注入方式和三级缓存时序,Bean 覆盖看注册顺序和条件评估报告;isActualTransactionActive 打点、BeanName 条件断点、--debug 报告是三件随身武器。到这篇为止,Spring 模块从容器原理到生产排查的 34 篇就全部完成了,接下来进入 MySQL 模块,从索引结构开始。
参考
参考:Spring Framework 源码
AbstractAutowireCapableBeanFactory、DefaultSingletonBeanRegistry(三级缓存实现);Spring Boot 文档 "Features >> Spring Application >> Startup Failure";31. AOP 与事务源码