一次 Spring 循环依赖排查复盘:为什么同事的 Jar 能跑,而我的本地启动却报错?
前言
最近在排查 service-oop 服务启动问题时,应用在 Spring 容器初始化阶段直接失败。
日志中出现如下异常:
BeanCreationException: Error creating bean with name 'thPlanMsgHandle'
nested exception is BeanCurrentlyInCreationException:
Error creating bean with name 'thTakeoffMsgHandler'
乍一看这是一个普通的 Spring 循环依赖问题,但现场却出现了一个非常容易误导排查方向的现象:
-
同事没有修改任何代码,打包出来的 Jar 可以正常运行;
-
我本地启动同一份代码却报循环依赖;
-
第一反应甚至怀疑是不是执行了
mvn install但忘了clean。
本文记录一下完整排查过程,以及最终定位和解决问题的思路。
一、问题现象
启动日志中的核心异常如下:
Error creating bean with name 'thPlanMsgHandle':
Injection of resource dependencies failed
nested exception is BeanCurrentlyInCreationException:
Error creating bean with name 'thTakeoffMsgHandler':
Bean with name 'thTakeoffMsgHandler'
has been injected into other beans
[reputationScoreService]
in its raw version as part of a circular reference,
but has eventually been wrapped
日志包含几个关键信息:
-
thPlanMsgHandle创建失败; -
创建过程中牵出了
thTakeoffMsgHandler; -
thTakeoffMsgHandler尚未完成代理包装时就被提前注入; -
相关 Bean 包括
reputationScoreService。
这类异常往往说明:
Bean 之间不仅存在循环依赖,同时还叠加了事务代理、AOP 代理等增强逻辑。
二、先梳理依赖关系
遇到循环依赖问题,第一件事不要急着修改代码。
先把依赖关系画出来。
本次涉及的核心类有:
ReputationScoreService
ThPlanMsgHandle
ThTakeoffMsgHandler
梳理代码后得到如下依赖关系:
ReputationScoreService
├─> ThPlanMsgHandle
└─> ThTakeoffMsgHandler
ThPlanMsgHandle
└─> ThTakeoffMsgHandler
ThTakeoffMsgHandler
└─> ThPlanMsgHandle
可以清晰看到:
ThPlanMsgHandle
↓
ThTakeoffMsgHandler
↓
ThPlanMsgHandle
已经形成了闭环。
即:
A → B → A
而 ReputationScoreService 又同时依赖了这两个 Bean,使得启动阶段更容易触发 Spring 的代理冲突。
三、为什么这不是一个 “没 clean” 的问题?
排查过程中最容易出现的误区就是:
我是不是执行了
mvn install,但是忘了执行clean?
事实上:
mvn clean install
中的:
clean
作用仅仅是:
删除 target 目录
而:
install
作用是:
重新编译并安装到本地 Maven 仓库
它们不会改变:
Spring Bean 依赖关系
更不会自动修复:
循环依赖
因此:
clean install
可能解决旧产物问题,
但不可能解决真实存在的 Bean 循环依赖。
四、为什么同事的 Jar 能跑?
这是本次排查过程中最容易让人困惑的地方。
1. 不一定运行的是同一份产物
很多时候大家说:
用的是同一份代码。
实际上可能并不是同一个运行产物。
例如:
同事:
CI 打包 Jar
自己:
IDEA 本地启动
或者:
同事:
旧 SNAPSHOT
自己:
最新 SNAPSHOT
只要依赖树稍有差异,结果就可能完全不同。
2. Bean 初始化顺序不同
Spring 启动过程中:
Bean 创建顺序
代理生成时机
类加载顺序
并不是绝对固定的。
尤其是涉及:
@Transactional
@Async
ApplicationContext.getBean(...)
AOP
时。
循环依赖问题很容易出现:
某些环境能启动
某些环境不能启动
的现象。
3. 外部配置差异
即使代码一致,下面这些因素也可能影响结果:
JDK版本
Spring Profile
Nacos配置
本地Maven缓存
启动方式
例如:
同事:
java -jar
自己:
IDEA Run
就可能出现不同结果。
五、本次修复方案
既然问题核心是:
ThPlanMsgHandle
↔
ThTakeoffMsgHandler
互相注入。
那么最小改动方案就是:
@Resource
@Lazy
private ThTakeoffMsgHandler thTakeoffMsgHandler;
以及:
@Resource
@Lazy
private ThPlanMsgHandle thPlanMsgHandle;
同时清理未使用的重复注入字段。
六、为什么 @Lazy 能解决?
很多同学会误认为:
@Lazy
能够消灭循环依赖。
实际上并不是。
它只是:
推迟依赖解析时机
原来:
启动时立即创建对方 Bean
改成:
启动时先注入代理对象
真正调用时再获取目标 Bean
即:
启动阶段
A -> Proxy(B)
运行阶段
Proxy(B) -> B
这样就绕过了 Spring 初始化阶段的死循环。
七、更彻底的优化方案
虽然 @Lazy 能解决当前问题,但从架构角度来看,更推荐拆分职责。
1. 去掉处理器之间的双向依赖
当前:
ThPlanMsgHandle
↓
ThTakeoffMsgHandler
ThTakeoffMsgHandler
↓
ThPlanMsgHandle
建议改为:
CoordinatorService
↓
┌────┴────┐
↓ ↓
Plan Takeoff
即:
A → Coordinator ← B
避免:
A ↔ B
2. 降低 Service 依赖数量
当前:
ReputationScoreService
同时依赖多个业务处理器。
依赖图越大:
启动越慢
排查越难
循环依赖风险越高
建议后续进一步拆分。
3. 谨慎使用 ApplicationContext.getBean()
本次代码中存在:
ApplicationContext.getBean(...)
用于获取自身代理对象保证事务生效。
这种方案虽然常见,但会导致:
Bean 创建路径复杂
调试困难
循环依赖更隐蔽
后续可以考虑优化事务边界设计。
八、循环依赖排查经验总结
以后再遇到类似问题,可以按照下面步骤排查。
Step1:先看最内层异常
重点关注:
BeanCurrentlyInCreationException
不要只看:
BeanCreationException
Step2:画依赖图
例如:
A
↓
B
↓
C
↓
A
很多问题画出来就清晰了。
Step3:区分环境问题和代码问题
环境问题:
依赖冲突
SNAPSHOT不一致
本地缓存污染
代码问题:
循环依赖
事务代理冲突
AOP代理冲突
两者不要混为一谈。
Step4:先恢复服务,再考虑重构
优先级建议:
1. 服务恢复启动
2. 业务恢复可用
3. 再做结构优化
因此本次优先采用:
@Lazy
而不是直接大规模重构。
总结
这次问题最大的价值不在于修掉了一个启动异常,而在于再次验证了一个经验:
能运行,不代表没有问题;能复现,也不一定全是环境问题。
最终定位结果:
ThPlanMsgHandle
↓
ThTakeoffMsgHandler
↓
ThPlanMsgHandle
形成循环依赖。
而:
clean
可以清理旧产物,
却无法消除真实存在的 Bean 依赖环。
因此以后遇到:
同事的 Jar 能跑,而我的本地启动报循环依赖
不要第一时间怀疑 Maven,而应该:
-
先确认是否同一份产物;
-
再确认配置和依赖是否一致;
-
最后回到代码结构本身,检查是否存在循环依赖。
归根结底:
clean能清掉旧产物,但清不掉循环依赖。
更多推荐



所有评论(0)