
问题背景老项目升级 Spring Boot 后经常在启动时看到The dependencies of some of the beans in the application context form a cycle很多人的第一反应是打开spring:main:allow-circular-references:true或者给其中一个注入点加Lazy。这些方法有时能让服务启动但它们回答的只是“怎样暂时创建 Bean”并没有回答“为什么两个业务服务必须互相依赖”。Lazy是否可以使用可以但建议把它定义为诊断手段或短期过渡方案。合理场景升级过程中需要先启动应用继续发现后面的兼容问题。循环链较长暂时无法确定最小拆分点。生产修复窗口很短需要先恢复服务再安排重构。不合理场景大量 Service 两两互相Lazy。把全局允许循环依赖作为永久配置。不记录依赖环和后续拆分任务。如何分析循环依赖假设存在SystemAdminService ↓ SystemRoleService ↓ SystemAdminService先检查双方实际使用的能力。经常会发现一方只需要查询角色名称。查询管理员基本信息。拼接附件路径。读取少量配置。这些能力不需要依赖完整业务 Service。三种常用拆分方式1. 拆只读 Query ServiceSystemAdminService ──→ SystemRoleQueryService SystemRoleService ──→ SystemAdminQueryServiceQuery Service 只提供稳定的读取能力不包含复杂写事务和反向业务调用。适合角色、管理员、配置、字典等高频读取场景。2. 拆无状态领域能力例如附件服务同时承担附件 CRUD。CDN 地址拼接。本地路径前缀移除。被分类、商品、订单等大量服务调用。可以将纯路径逻辑拆成AttachmentPathService无状态服务不再依赖附件业务 Service能直接切断多个依赖环。3. 用事件解耦后续动作如果 A 完成后需要通知 B 执行独立动作并且不要求同一个调用栈立即返回可以考虑领域事件或应用事件。但不要为了消除任何循环都改成异步事件。涉及强事务一致性的操作仍需明确边界。推荐实施流程第一步记录完整依赖环不要只看异常最后两个类。循环可能是A → B → C → D → A第二步单侧Lazy恢复启动选择风险较低、调用较少的一侧作为临时代理点。启动后继续收集其他循环而不是立即宣布问题解决。第三步统计调用方法列出依赖对象实际调用的方法。如果只调用一两个查询或格式化方法优先拆小接口。第四步按职责拆分拆分后应满足新服务职责单一。不依赖原始双方的完整实现。不为了“解环”复制业务规则。事务边界仍然明确。第五步移除临时方案删除单侧Lazy或全局循环依赖开关再次启动验证。常见误区用接口替换实现类就能解环Spring 注入接口仍会创建对应实现 Bean。如果实现之间互相依赖改成接口并不会消除循环。构造器注入导致循环改字段注入即可字段注入可能把错误推迟到运行期并没有改善设计。构造器注入反而更早暴露真实依赖。所有 Query Service 都直接访问 MapperQuery Service 是否可以访问 Mapper要结合项目分层约定。重点是避免它重新依赖完整业务 Service形成新的循环。总结Boot 升级只是让循环依赖变得可见。正确策略是用单侧Lazy恢复诊断能力再通过只读查询服务、无状态领域能力或合理事件拆分完成正式解环。