
1. 循环依赖的本质与Spring处理机制在Spring框架中循环依赖指的是两个或多个Bean相互依赖对方完成初始化的情况。比如Bean A的构造需要注入Bean B而Bean B的构造又需要注入Bean A。这种鸡生蛋蛋生鸡的问题在传统编程模式下无解但Spring通过三级缓存机制提供了优雅的解决方案。Spring的三级缓存包括singletonObjects存放完全初始化好的单例BeanearlySingletonObjects存放提前暴露的原始Bean尚未填充属性singletonFactories存放Bean的ObjectFactory用于处理AOP代理当遇到循环依赖时Spring会先创建Bean A的实例此时还未填充属性将Bean A的ObjectFactory放入三级缓存开始填充Bean A的属性发现需要Bean B创建Bean B的实例同样先放入三级缓存填充Bean B的属性时从三级缓存获取Bean A的早期引用完成Bean B的初始化后Bean A也能完成初始化关键点三级缓存允许Spring在Bean完全初始化前就暴露引用打破循环链条。但这个过程在遇到AOP代理时会变得复杂。2. AOP代理如何影响循环依赖处理当Bean需要被AOP代理时问题会变得复杂。因为Spring需要决定应该在哪个阶段创建代理对象如何确保循环依赖的双方拿到的是同一个代理实例Spring的默认行为是在Bean初始化完成后postProcessAfterInitialization创建代理但对于循环依赖的Bean必须在早期就创建代理否则依赖方拿到的会是原始对象这导致两种代理创建时机普通Bean初始化完成后创建代理循环依赖的Bean在getEarlyBeanReference阶段就创建代理// AbstractAutowireCapableBeanFactory中的关键代码 protected Object getEarlyBeanReference(String beanName, Object bean) { Object exposedObject bean; for (SmartInstantiationAwareBeanPostProcessor bp : getBeanPostProcessorCache().smartInstantiationAware) { exposedObject bp.getEarlyBeanReference(exposedObject, beanName); } return exposedObject; }3. 典型AOP循环依赖问题场景3.1 自我注入场景Service public class UserService { Autowired private UserService self; // 自我注入 Transactional // 需要AOP代理 public void methodA() { // ... } }这个案例中UserService需要被代理因为有Transactional同时又依赖自身如果代理创建时机不当会导致注入的是原始对象而非代理3.2 双向依赖场景Service public class ServiceA { Autowired private ServiceB serviceB; Async // 需要AOP代理 public void methodA() { // ... } } Service public class ServiceB { Autowired private ServiceA serviceA; }这个案例的复杂性在于ServiceA需要被代理AsyncServiceB又依赖ServiceAServiceA同时依赖ServiceB4. 解决方案与实战技巧4.1 使用Lazy注解Service public class ServiceA { Lazy // 关键解决方案 Autowired private ServiceB serviceB; }Lazy的工作原理不立即解析依赖而是注入一个代理对象当第一次调用方法时才真正触发依赖解析此时循环依赖链已经断开可以正常初始化最佳实践在循环依赖的任意一方添加Lazy即可通常加在字段上比加在类上更精准。4.2 调整Bean加载顺序DependsOn(serviceB) // 明确指定依赖顺序 Service public class ServiceA { // ... }这种方法强制Spring先初始化ServiceB适合依赖关系明确的场景不能解决所有循环依赖问题4.3 使用Setter注入替代构造器注入Service public class ServiceA { private ServiceB serviceB; Autowired // 改为setter注入 public void setServiceB(ServiceB serviceB) { this.serviceB serviceB; } }Setter注入的优势允许先创建Bean实例再解决依赖与字段注入相比提供了更灵活的初始化时机4.4 重构设计避免循环依赖从根本上解决问题的方案提取公共逻辑到第三个服务中使用事件驱动架构ApplicationEvent引入门面模式统一对外接口5. 原理级深度解析5.1 Spring处理AOP循环依赖的完整流程以ServiceA → ServiceB → ServiceA为例开始创建ServiceA实例化ServiceA原始对象将ServiceA的ObjectFactory放入三级缓存填充ServiceA属性发现需要ServiceB开始创建ServiceB实例化ServiceB原始对象将ServiceB的ObjectFactory放入三级缓存填充ServiceB属性发现需要ServiceA从三级缓存获取ServiceA的早期引用此时会通过AbstractAutoProxyCreator创建ServiceA的代理完成ServiceB初始化完成ServiceA初始化此时会检查是否需要重新创建代理如果之前已经创建过则复用5.2 关键源码分析在DefaultAopProxyFactory中public AopProxy createAopProxy(AdvisedSupport config) { if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) { // 使用CGLIB代理 return new CglibAopProxy(config); } else { // 使用JDK动态代理 return new JdkDynamicAopProxy(config); } }代理选择策略目标类实现了接口 → JDK动态代理目标类未实现接口 → CGLIB可通过EnableAspectJAutoProxy(proxyTargetClasstrue)强制使用CGLIB6. 生产环境中的疑难问题排查6.1 代理不一致问题症状部分方法AOP生效部分不生效日志显示调用了原始对象的方法排查步骤检查Bean的toString()输出确认是否是代理对象在调试模式下查看Bean的类名代理类会有$$EnhancerBySpringCGLIB$$后缀确认所有注入点是否一致使用接口类型6.2 事务不生效的常见原因自调用问题同类中非代理方法调用代理方法异常类型未被正确捕获默认只回滚RuntimeException事务传播行为配置不当解决方案示例Service public class OrderService { Autowired private ApplicationContext context; private OrderService self; // 用于自调用 PostConstruct public void init() { self context.getBean(OrderService.class); } public void outerMethod() { self.innerMethod(); // 通过代理对象调用 } Transactional public void innerMethod() { // 事务操作 } }7. 性能优化与最佳实践7.1 代理方式选择建议场景推荐代理方式理由类有接口实现JDK动态代理运行时生成快调用稍慢类无接口CGLIB必须使用需要代理final方法无解两种代理都不支持大量代理场景CGLIB调用性能更好7.2 循环依赖设计规范控制依赖方向为单向避免双向依赖层次化架构Controller → Service → Repository使用事件总线解耦服务间通信定期使用ArchUnit检查架构约束示例ArchUnit测试ArchTest public static final ArchRule no_cycles slices().matching(com.myapp.(*)..) .should().beFreeOfCycles();8. Spring Boot中的特殊配置8.1 强制使用CGLIB代理# application.properties spring.aop.proxy-target-classtrue8.2 循环依赖检测开关# 开发环境建议开启 spring.main.allow-circular-referencestrue生产环境建议即使开启了这个开关也应该消除循环依赖因为它会导致启动时间变长内存占用增加系统可维护性降低9. 新版Spring的变化Spring Framework 6.0中的改进原生镜像支持GraalVM下对代理的处理优化更精确的循环依赖错误报告对记录式record类的代理支持迁移注意事项检查过时的ProxyFactoryBean用法验证自定义TargetSource的实现测试AspectJ切面的兼容性10. 替代方案与架构思考对于复杂系统可以考虑使用CDIContexts and Dependency Injection切换到Micronaut编译时AOP处理采用Quarkus框架比较表特性SpringMicronautQuarkus启动时间慢快最快内存占用高低最低循环依赖处理支持不支持有限支持学习曲线平缓中等陡峭最后需要强调的是虽然Spring提供了处理循环依赖的机制但在设计层面应该尽量避免。我在实际项目中的经验是使用SonarQube等工具静态检测循环依赖在代码评审时特别关注Service之间的引用对于不可避免的循环依赖必须添加详细的注释说明原因考虑引入ArchUnit等架构测试工具在CI流程中自动阻断新增的循环依赖