前言

最近在排查 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,而应该:

  1. 先确认是否同一份产物;

  2. 再确认配置和依赖是否一致;

  3. 最后回到代码结构本身,检查是否存在循环依赖。

归根结底:

clean 能清掉旧产物,但清不掉循环依赖。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐