有过经验的同学应该都知道Spring能够自动解决循环依赖的问题,依靠的是它为单例池提供的三级缓存。如果你还不清楚三级缓存具体是怎么个解法的话,可以看一下这篇文章【图文详解】Spring是如何解决循环依赖的?
本文中的问题来源于我在开发项目时,偶然碰到了循环依赖的报错,错误内容如下所示,如果你也遇到了类似的报错,那恭喜你找对地方了
org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'AService': Bean with name 'AService' has been injected into other beans [BService] in its raw version as part of a circular reference, but has eventually been wrapped. This means that said other beans do not use the final version of the bean. This is often the result of over-eager type matching - consider using 'getBeanNamesForType' with the 'allowEagerInit' flag turned off, for example.
这个报错的意思是:AService 这个 Bean 的原始版本被注入到了 BService 中,但是 AService 最终又被包装进行了代理。也就是说 BService中持有了 AService 的原始对象而不是最终生成的代理对象,这肯定是不得行的,所以 Spring 在启动时就报错了。
你可能比较疑惑,Spring不是自己能处理循环依赖么?产生上述问题的原因如下:Spring的单例池三级缓存,在第三级缓存其实可以解决处理一部分代理场景,比如@Transaction 和我们自定义的切面,但是仅是一部分,或者说绝大部分,但不是全部!像@Async这种注解生成的代理就不在第三级缓存的监测范围内,而是会在Bean的初始化阶段(initializeBean)针对@Async生成对应的代理。所以Spring通过单例池三级缓存解决循环依赖时处理不了@Async这种代理情况,最终导致上面的异常。
补充一下,在SpringBoot的2.x版本下,会产生上述问题。SpringBoot最新的3.x版本已经解决了这个问题。
我的问题原因就是因为在产生循环依赖的类里使用了 @Async注解。
解决办法:
- 升级SpriingBoot版本到最新版本。我尝试了 3.5.0 版本,没有再出现这个问题,但是需要将jdk版本升级到17(SpringBoot 3.5.0版本要求),如果你的线上jdk版本较低的话,可以放弃这个办法。
- 使用@Lazy注解。在产生循环依赖的 @Autowired 上加上 @Lazy 注解,让这些属性不在Spring容器启动时加载,而是延迟到真正使用这些属性时再加载,这时候Bean都已经创建好了,该代理的也早就代理过了,不会再出现上述问题。但是注意当带@Async注解的类 再和 其他类又产生循环依赖关系时也要加上@Lazy注解。
- 重新规划代码,将带@Async的方法提到一个单独的类中,不和其他类产生循环依赖。
下面是一个源码跟踪的详细步骤,感兴趣的可以接着看下
先定义两个产生循环依赖的Service类, 两个Service类都只有一个方法 say, 并且AService 的 say 方法添加了 @Async 注解;两个Service类 分别引用了对方,产生了循环依赖。
@Service
public class AService {@Autowiredprivate BService bService;@Asyncpublic void say() {System.out.println("AService -- say");}}@Service
public class BService {@Autowiredprivate AService aService;public void say() {System.out.println("BService -- say");}}
这种情况下容器启动时就会报错
2025-05-28 16:26:41.970 ERROR 20343 --- [ restartedMain] o.s.boot.SpringApplication : Application run failedorg.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'AService': Bean with name 'AService' has been injected into other beans [BService] in its raw version as part of a circular reference, but has eventually been wrapped. This means that said other beans do not use the final version of the bean. This is often the result of over-eager type matching - consider using 'getBeanNamesForType' with the 'allowEagerInit' flag turned off, for example.at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:649) ~[spring-beans-5.3.12.jar:5.3.12]at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542) ~[spring-beans-5.3.12.jar:5.3.12]at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335) ~[spring-beans-5.3.12.jar:5.3.12]at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-5.3.12.jar:5.3.12]at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333) ~[spring-beans-5.3.12.jar:5.3.12]at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208) ~[spring-beans-5.3.12.jar:5.3.12]at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:944) ~[spring-beans-5.3.12.jar:5.3.12]at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:918) ~[spring-context-5.3.12.jar:5.3.12]at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:583) ~[spring-context-5.3.12.jar:5.3.12]at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:145) ~[spring-boot-2.5.6.jar:2.5.6]at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:754) [spring-boot-2.5.6.jar:2.5.6]at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:434) [spring-boot-2.5.6.jar:2.5.6]at org.springframework.boot.SpringApplication.run(SpringApplication.java:338) [spring-boot-2.5.6.jar:2.5.6]at org.springframework.boot.SpringApplication.run(SpringApplication.java:1343) [spring-boot-2.5.6.jar:2.5.6]at org.springframework.boot.SpringApplication.run(SpringApplication.java:1332) [spring-boot-2.5.6.jar:2.5.6]at com.hml.SpringBootSimpleApplication.main(SpringBootSimpleApplication.java:16) [classes/:na]at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_372]at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_372]at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_372]at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_372]at org.springframework.boot.devtools.restart.RestartLauncher.run(RestartLauncher.java:49) [spring-boot-devtools-2.5.6.jar:2.5.6]
下面我采用刚才提到的解法2:添加@Lazy注解来试试。
这是改造后的两个Service, 仅仅是在 AService的 @Autowired private BService bService; 属性上加上了 @Lazy 注解。
@Service
public class AService {@Autowired@Lazyprivate BService bService;@Asyncpublic void say() {System.out.println("AService -- say");}}@Service
public class BService {@Autowiredprivate AService aService;public void say() {System.out.println("BService -- say");}}
这时候再启动项目就不会再报错了。下来通过源码来解释下原因
Spring容器在启动时会默认提前加载所有Bean到单例池,并且如果Bean中有 @Autowired 引用其他类的话,也会将对应类的Bean创建出来,并将其对象引用拿过来放到 @Autowired 属性中。
那AService和BService谁先加载呢?在AbstractBeanFactory类的doGetBean方法上打断点,在我们定义的这两个Bean创建时将流程停住。
启动后可以发现,Spring先加载了AService。在AbstractAutowireCapableBeanFactory的doCreateBean方法中会执行Bean生命周期的关键三步 1.实例化Bean 2.给Bean填充属性 3.初始化Bean。
AService 创建Bean的过程中,在执行到第二步 populateBean 时,发现有个 @Autowired BServce bService属性,于是开始获取 BService 这个 Bean, 由于BService这个Bean还未创建,所以就开始了创建 BService这个Bean的流程。在创建BService Bean时,发现它又要注入 AService, 于是又反过来去获取 AService这个Bean, 然后就来到了问题关键点,AService 这个Bean当前被放在单例池第三级缓存中,三级缓存中放的是个ObjectFactory, 它通过 getObject 方法来获取Bean, 这个getObject方法是通过一个 lambda表达式来声明的,即通过getEarlyBeanReference来获取Bean, getEarlyBeanReference方法会判断这个Bean需不需要被代理,如果需要被代理的话就在这个方法里生成一个代理后的Bean。但是这一步只会有两个BeanPostProcessor来处理Bean的代理逻辑:
org.springframework.aop.framework.autoproxy.InfrastructureAdvisorAutoProxyCreator
org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor
而这两个BeanPostProcessor 都无法针对 @Async 生成代理,最终getEarlyBeanReference方法执行完后发现 AService 不需要被代理😮,于是返回了AService的原始对象,最终这个原始对象的引用被放到了 BService 的 @Autowired AService aService; 属性里。
随后BService 这个Bean创建完了, AService 拿到 BService的引用, 放到它的 @Autowired BService bService属性中了, 然后 AService 开始进行“初始化”(initializeBean)过程,在“初始化”过程中会再进行判断这个 Bean 需不需要被代理,当然 initializeBean 过程中判断代理用到的 BeanPostProcessor 不仅包含 单例池第三级缓存 判断代理用到的 BeanPostProcessor,而且更多,足足有12个BeanPostProcessor,其中就包括 AsyncAnnotationBeanPostProcessor,这个类就是用来处理 @Async 代理的。于是 AService 在执行完“初始化”过程后就生成了一个代理后的Bean, 看!问题就出来了,AService 最终需要被放到单例池中(注意此时AService还没放入单例池)的是代理后的Bean, 但是 BService持有的却是 代理前的AService Bean, Spring在判断到这个情况后就报异常了

两个Bean创建的时序图如下
知道了原因,就知道为什么我们用@Lazy注解可以解决这个问题了, @Lazy注解修饰 @Autowired后,AService中的 bService属性就会延时加载,在AService创建Bean的过程中就不会去寻找 BService Bean的引用,创建完 AService Bean后 单例池中的 AService Bean就已经是代理过的对象了 ,AService的代理对象也建好了, 后续代码逻辑中再触发获取AService Bean时就直接能从单例池中获取代理后的AService了。
使用@Lazy注解属于头痛医头脚痛医脚的治疗方式,个人觉得最好的方式还是重新规划下代码,将使用 @Async 注解的方法单独提到一个干净的类里并且不和其他类形成循环依赖。