从一次启动失败深入剖析:Spring循环依赖的真相|得物技术
结果:项目启动失败抛出异常BeanCurrentlyInCreationException
Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'a': Requested bean is currently in creation: Is there an unresolvable circular reference?at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.beforeSingletonCreation(DefaultSingletonBeanRegistry.java:339)at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:215)at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:318)at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199)
构造器注入构成的循环依赖,此种循环依赖方式无论是Singleton模式还是prototype模式都是无法解决的,只能抛出BeanCurrentlyInCreationException异常表示循环依赖。原因是Spring解决循环依赖依靠的是Bean的“中间态”这个概念,而中间态指的是已经实例化,但还没初始化的状态。而完成实例化需要调用构造器,所以构造器的循环依赖无法解决。
Singleton模式field属性注入(setter方法注入)循环依赖:
这种方式是我们最为常用的依赖注入方式:
@Servicepublic class A {@Autowiredprivate B b;}@Servicepublic class B {@Autowiredprivate A a;}
结果:项目启动成功,正常运行
prototype field属性注入循环依赖:
prototype在平时使用情况较少,但是也并不是不会使用到,因此此种方式也需要引起重视。
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)@Servicepublic class A {@Autowiredprivate B b;}@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)@Servicepublic class B {@Autowiredprivate A a;}
结果:需要注意的是本例中启动时是不会报错的(因为非单例Bean默认不会初始化,而是使用时才会初始化),所以很简单咱们只需要手动getBean()或者在一个单例Bean内@Autowired一下它即可。
// 在单例Bean内注入@Autowiredprivate A a;
这样子启动就报错:
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'mytest.TestSpringBean': Unsatisfied dependency expressed through field 'a'; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'a': Unsatisfied dependency expressed through field 'b'; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'b': Unsatisfied dependency expressed through field 'a'; nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'a': Requested bean is currently in creation: Is there an unresolvable circular reference?at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:596)at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:90)at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessProperties(AutowiredAnnotationBeanPostProcessor.java:374)
如何解决?可能有的小伙伴看到网上有说使用@Lazy注解解决:
@Lazy@Autowiredprivate A a;
此处负责任的告诉你这样是解决不了问题的(可能会掩盖问题),@Lazy只是延迟初始化而已,当你真正使用到它(初始化)的时候,依旧会报如上异常。
对于Spring循环依赖的情况总结如下:
不能解决的情况:构造器注入循环依赖,prototype field属性注入循环依赖
能解决的情况:field属性注入(setter方法注入)循环依赖
Spring如何解决循环依赖
Spring 是通过三级缓存和提前曝光的机制来解决循环依赖的问题。
三级缓存
三级缓存其实就是用三个 Map 来存储不同阶段 Bean 对象。
一级缓存private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);二级缓存private final Map<String, ObjectearlySingletonObjects = new HashMap<>(16);//三级缓存private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16)
singletonObjects:用于存放完全初始化好的 bean,从该缓存中取出的 bean 可以直接使用。
earlySingletonObjects:提前曝光的单例对象的cache,存放原始的 bean 对象(尚未填充属性),用于解决循环依赖。
singletonFactories:单例对象工厂的cache,存放 bean 工厂对象,用于解决循环依赖。
三级缓存解决循环依赖过程
假设现在我们有ServiceA和ServiceB两个类,这两个类相互依赖,代码如下:
@Servicepublic class ServiceA {@Autowiredprivate ServiceB serviceB;}@Servicepublic class ServiceB {@Autowiredprivate ServiceA serviceA ;}
下面的时序图说明了spring用三级缓存解决循环依赖的主要流程:
为什么需要三级缓存?
这是一个理解Spring容器如何解决循环依赖的核心概念。三级缓存是Spring为了解决循环依赖的同时,又能保证AOP代理的正确性而设计的精妙机制。
为了理解为什么需要三级缓存,我们一步步来看。
如果没有缓存(Level 0)
假设有两个Bean:ServiceA 和 ServiceB,它们相互依赖。
Java
@Componentpublic class ServiceA {@Autowiredprivate ServiceB serviceB;}@Componentpublic class ServiceB {@Autowiredprivate ServiceA serviceA;}
创建过程(无缓存):
开始创建 ServiceA -> 发现 ServiceA 需要 ServiceB -> 开始创建 ServiceB
开始创建 ServiceB -> 发现 ServiceB 需要 ServiceA -> 开始创建 ServiceA
开始创建 ServiceA -> 发现 ServiceA 需要 ServiceB -> ... 无限循环,StackOverflowError
结论:无法解决循环依赖,直接死循环。
如果只有一级缓存(Singleton Objects)
一级缓存存放的是已经完全创建好、初始化完毕的Bean。
问题:在Bean的创建过程中(比如在填充属性 populateBean 时),ServiceA还没创建完,它本身不应该被放入"已完成"的一级缓存。但如果ServiceB需要ServiceA,而一级缓存里又没有ServiceA的半成品,ServiceB就无法完成创建。这就回到了上面的死循环问题。
结论:一级缓存无法解决循环依赖。
如果使用二级缓存
二级缓存的核心思路是:将尚未完全初始化好的“早期引用”暴露出来。
现在我们有:
一级缓存(成品库):存放完全准备好的Bean。
二级缓存(半成品库):存放刚刚实例化(调用了构造方法),但还未填充属性和初始化的Bean的早期引用。
创建过程(二级缓存):
开始创建ServiceA:
实例化ServiceA(调用ServiceA的构造方法),得到一个ServiceA的原始对象。
将ServiceA的原始对象放入二级缓存(半成品库)。
开始为ServiceA填充属性 -> 发现需要ServiceB。
开始创建ServiceB:
实例化ServiceB(调用B的构造方法),得到一个ServiceB的原始对象。
将ServiceB的原始对象放入二级缓存。
开始为ServiceB填充属性 -> 发现需要ServiceA。
ServiceB从二级缓存中获取A:
ServiceB成功从二级缓存中拿到了ServiceA的早期引用(原始对象)。
ServiceB顺利完成了属性填充、初始化等后续步骤,成为一个完整的Bean。
将完整的ServiceB放入一级缓存(成品库),并从二级缓存移除ServiceB。
ServiceA继续创建:
ServiceA拿到了创建好的ServiceB,完成了自己的属性填充和初始化。
将完整的ServiceA放入一级缓存(成品库),并从二级缓存移除ServiceA。
问题来了:如果ServiceA需要被AOP代理怎么办?
如果A类上加了 @Transactional 等需要创建代理的注解,那么最终需要暴露给其他Bean的应该是ServiceA的代理对象,而不是ServiceA的原始对象。
在二级缓存方案中,ServiceB拿到的是A的原始对象。但最终ServiceA完成后,放入一级缓存的是ServiceA的代理对象。这就导致了:
ServiceB里面持有的ServiceA是原始对象。
而其他地方注入的ServiceA是代理对象。
这就造成了不一致!如果通过ServiceB的ServiceA去调用事务方法,事务会失效,因为那是一个没有被代理的原始对象。
结论:二级缓存可以解决循环依赖问题,但无法正确处理需要AOP代理的Bean。
三级缓存的登场(Spring的终极方案)
为了解决代理问题,Spring引入了第三级缓存。它的核心不是一个直接存放对象(Object)的缓存,而是一个存放 ObjectFactory(对象工厂)的缓存。
三级缓存的结构是:Map<String, ObjectFactory<?>> singletonFactories
创建过程(三级缓存,以ServiceA需要代理为例):
开始创建ServiceA:
实例化ServiceA,得到ServiceA的原始对象。
向三级缓存添加一个ObjectFactory。这个工厂的getObject()方法有能力判断ServiceA是否需要代理,并返回相应的对象(原始对象或代理对象)。
开始为ServiceA填充属性 -> 发现需要ServiceB。
开始创建B:
实例化ServiceB。
同样向三级缓存添加一个ServiceB的ObjectFactory。
开始为ServiceB填充属性 -> 发现需要ServiceA。
ServiceB从缓存中获取ServiceA:
ServiceB发现一级缓存没有ServiceA,二级缓存也没有ServiceA。
ServiceB发现三级缓存有A的ObjectFactory。
B调用这个工厂的getObject()方法。此时,Spring会执行一个关键逻辑:
如果ServiceA需要被代理,工厂会提前生成ServiceA的代理对象并返回。
如果ServiceA不需要代理,工厂则返回A的原始对象。
将这个早期引用(可能是原始对象,也可能是代理对象)放入二级缓存,同时从三级缓存移除A的工厂。
ServiceB拿到了ServiceA的正确版本的早期引用。
后续步骤:
ServiceB完成创建,放入一级缓存。
ServiceA继续用ServiceB完成创建。在ServiceA初始化的最后,Spring会再次检查:如果ServiceA已经被提前代理了(即在第3步中),那么就直接返回这个代理对象;如果没有,则可能在此处创建代理(对于不需要解决循环依赖的Bean)。
最终,将完整的ServiceA(代理对象)放入一级缓存,并清理二级缓存。
总结:为什么需要三级缓存?
需要三级缓存,是因为Spring要解决一个复杂问题:在存在循环依赖的情况下,如何确保所有Bean都能拿到最终形态(可能被AOP代理)的依赖对象,而不是原始的、未代理的对象。 三级缓存通过一个ObjectFactory将代理的时机提前,完美地解决了这个问题。二级缓存主要是为了性能优化而存在的。
spring三级缓存为什么不能解决
@Async注解的循环依赖问题
这触及了 Spring 代理机制的一个深层次区别。@Async注解的循环依赖问题确实比@Transactional 更复杂,三级缓存无法完全解决。让我们深入分析原因。
Spring创建Bean主要流程
为了容易理解 Spring 解决循环依赖过程,我们先简单温习下 Spring 容器创建 Bean 的主要流程。
从代码看Spring对于Bean的生成过程,步骤还是很多的,我把一些扩展业务代码省略掉:
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)throws BeanCreationException {if (mbd.isSingleton()) {instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);}// Bean初始化第一步:默认调用无参构造实例化Bean// 如果是只有带参数的构造方法,构造方法里的参数依赖注入,就是发生在这一步if (instanceWrapper == null) {instanceWrapper = createBeanInstance(beanName, mbd, args);}//判断Bean是否需要提前暴露对象用来解决循环依赖,需要则启动spring三级缓存boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&isSingletonCurrentlyInCreation(beanName));if (earlySingletonExposure) {if (logger.isTraceEnabled()) {logger.trace("Eagerly caching bean '" + beanName +"' to allow for resolving potential circular references");}addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));}// Initialize the bean instance.Object exposedObject = bean;try {// bean创建第二步:填充属性(DI依赖注入发生在此步骤)populateBean(beanName, mbd, instanceWrapper);// bean创建第三步:调用初始化方法,完成bean的初始化操作(AOP的第三个入口)// AOP是通过自动代理创建器AbstractAutoProxyCreator的postProcessAfterInitialization()//方法的执行进行代理对象的创建的,AbstractAutoProxyCreator是BeanPostProcessor接口的实现exposedObject = initializeBean(beanName, exposedObject, mbd);if (earlySingletonExposure) {Object earlySingletonReference = getSingleton(beanName, false);if (earlySingletonReference != null) {if (exposedObject == bean) {exposedObject = earlySingletonReference;}else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {String[] dependentBeans = getDependentBeans(beanName);Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length);for (String dependentBean : dependentBeans) {if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {actualDependentBeans.add(dependentBean);}}if (!actualDependentBeans.isEmpty()) {throw new BeanCurrentlyInCreationException(beanName,"Bean with name '" + beanName + "' has been injected into other beans [" +StringUtils.collectionToCommaDelimitedString(actualDependentBeans) +"] 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 " +"'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.");}}}}} catch (Throwable ex) {// ...}// ...return exposedObject;}
从上述代码看出,整体脉络可以归纳成 3 个核心步骤:
实例化Bean:主要是通过反射调用默认构造函数创建 Bean 实例,此时Bean的属性都还是默认值null。被注解@Bean标记的方法就是此阶段被调用的。
填充Bean属性:这一步主要是对Bean的依赖属性进行填充,对@Value、@Autowired、@Resource注解标注的属性注入对象引用。
调用Bean初始化方法:调用配置指定中的init方法,如 xml文件指定Bean的init-method方法或注解 @Bean(initMethod = "initMethod")指定的方法。
三
案例分析
代码分析
以下是我简化后的类之间大体的依赖关系,工程内实际的依赖情况会比这个简化版本复杂一些。
@RestControllerpublic class OldCenterSpuController {@Resourceprivate NewSpuApplyCheckServiceImpl newSpuApplyCheckServiceImpl;}@RestControllerpublic class TimeoutNotifyController {@Resourceprivate SpuCheckDomainServiceImpl spuCheckDomainServiceImpl;}@Componentpublic class NewSpuApplyCheckServiceImpl {@Resourceprivate SpuCheckDomainServiceImpl spuCheckDomainServiceImpl;}@Component@Slf4j@Validatedpublic class SpuCheckDomainServiceImpl {@Resourceprivate NewSpuApplyCheckServiceImpl newSpuApplyCheckServiceImpl;}
从代码看,主要是SpuCheckDomainServiceImpl和NewSpuApplyCheckServiceImpl 构成了一个依赖环。而我们从正常启动的bean加载顺序发现首先是从OldCenterSpuController开始加载的,具体情况如下所示:
OldCenterSpuController↓ (依赖)NewSpuApplyCheckServiceImpl↓ (依赖)SpuCheckDomainServiceImpl↓ (依赖)NewSpuApplyCheckServiceImpl
异常启动的情况bean加载是从TimeoutNotifyController开始加载的,具体情况如下所示:
TimeoutNotifyController↓ (依赖)SpuCheckDomainServiceImpl↓ (依赖)NewSpuApplyCheckServiceImpl↓ (依赖)SpuCheckDomainServiceImpl
同一个依赖环,为什么从OldCenterSpuController 开始加载就可以正常启动,而从TimeoutNotifyController 启动就会启动异常呢?下面我们会从现场debug的角度来分析解释这个问题。
问题分析
在相关知识点简介里面知悉到spring用三级缓存解决了循环依赖问题。为什么后台服务admin启动还会报循环依赖的问题呢?
要得到问题的答案,还是需要回到源码本身,前面我们分析了spring的创建Bean的主要流程,这里为了更好的分析问题,补充下通过容器获取Bean的。
在通过spring容器获取bean时,底层统一会调用doGetBean方法,大体如下:
protected <T> T doGetBean(final String name, @Nullablefinal Class<T> requiredType,@Nullable final Object[] args, boolean typeCheckOnly) throws BeansException {final String beanName = transformedBeanName(name);Object bean;// 从三级缓存获取beanObject sharedInstance = getSingleton(beanName);if (sharedInstance != null && args == null) {bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);}else {if (mbd.isSingleton()) {sharedInstance = getSingleton(beanName, () -> {try {//如果是单例Bean,从三级缓存没有获取到bean,则执行创建bean逻辑return createBean(beanName, mbd, args);}catch (BeansException ex) {destroySingleton(beanName);throw ex;}});bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);}}
从doGetBean方法逻辑看,在spring从一二三级缓存获取bean返回空时,会调用createBean方法去场景bean,createBean方法底层主要是调用前面我们提到的创建Bean流程的doCreateBean方法。
注意:doGetBean方法里面getSingleton方法的逻辑是先从一级缓存拿,拿到为空并且bean在创建中则又从二级缓存拿,二级缓存拿到为空 并且当前容器允许有循环依赖则从三级缓存拿。并且将对象工厂移到二级缓存,删除三级缓存
doCreateBean方法如下:
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)throws BeanCreationException {if (mbd.isSingleton()) {instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);}// Bean初始化第一步:默认调用无参构造实例化Bean// 如果是只有带参数的构造方法,构造方法里的参数依赖注入,就是发生在这一步if (instanceWrapper == null) {instanceWrapper = createBeanInstance(beanName, mbd, args);}//判断Bean是否需要提前暴露对象用来解决循环依赖,需要则启动spring三级缓存boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&isSingletonCurrentlyInCreation(beanName));if (earlySingletonExposure) {if (logger.isTraceEnabled()) {logger.trace("Eagerly caching bean '" + beanName +"' to allow for resolving potential circular references");}addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));}// Initialize the bean instance.Object exposedObject = bean;try {// bean创建第二步:填充属性(DI依赖注入发生在此步骤)populateBean(beanName, mbd, instanceWrapper);// bean创建第三步:调用初始化方法,完成bean的初始化操作(AOP的第三个入口)// AOP是通过自动代理创建器AbstractAutoProxyCreator的postProcessAfterInitialization()//方法的执行进行代理对象的创建的,AbstractAutoProxyCreator是BeanPostProcessor接口的实现exposedObject = initializeBean(beanName, exposedObject, mbd);if (earlySingletonExposure) {Object earlySingletonReference = getSingleton(beanName, false);if (earlySingletonReference != null) {if (exposedObject == bean) {exposedObject = earlySingletonReference;}else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {String[] dependentBeans = getDependentBeans(beanName);Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length);for (String dependentBean : dependentBeans) {if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {actualDependentBeans.add(dependentBean);}}if (!actualDependentBeans.isEmpty()) {throw new BeanCurrentlyInCreationException(beanName,"Bean with name '" + beanName + "' has been injected into other beans [" +StringUtils.collectionToCommaDelimitedString(actualDependentBeans) +"] 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 " +"'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.");}}}}} catch (Throwable ex) {// ...}// ...return exposedObject;}
将doGetBean和doCreateBean的逻辑转换成流程图如下:
从流程图可以看出,后台服务admin启动失败抛出UnsatisfiedDependencyException异常的必要条件是存在循环依赖,因为不存在循环依赖的情况bean只会存在单次加载,单次加载的情况bean只会被放进spring的第三级缓存。
而触发UnsatisfiedDependencyException异常的先决条件是需要spring的第一二级缓存有当前的bean。所以可以知道当前bean肯定存在循环依赖。在存在循环依赖的情况下,当前bean被第一次获取(即调用doGetBean方法)会缓存进spring的第三级缓存,然后会注入当前bean的依赖(即调用populateBean方法),在当前bean所在依赖环内其他bean都不在一二级缓存的情况下,会触发当前bean的第二次获取(即调用doGetBean方法),由于第一次获取已经将Bean放进了第三级缓存,spring会将Bean从第三级缓存移到二级缓存并删除第三级缓存。
最终会回到第一次获取的流程,调用初始化方法做初始化。最终在初始化有对当前bean做代理增强的并且提前暴露到二级缓存的对象有被其他依赖引用到,而且allowRawInjectionDespiteWrapping=false的情况下,会导致抛出UnsatisfiedDependencyException,进而导致启动异常。
注意:在注入当前bean的依赖时,这里spring将Bean从第三级缓存移到二级缓存并删除第三级缓存后,当前bean的依赖的其他bean会从二级缓存拿到当前bean做依赖。这也是后续抛异常的先决条件
结合admin有时候启动正常,有时候启动异常的情况,这里猜测启动正常和启动异常时bean加载顺序不一致,进而导致启动正常时当前Bean只会被获取一次,启动异常时当前bean会被获取两次。为了验证猜想,我们分别针对启动异常和启动正常的bean获取做了debug。
debug分析
首先我们从启动异常提取到以下关键信息,从这些信息可以知道是spuCheckDomainServiceImpl的加载触发的启动异常。所以我们这里以spuCheckDomainServiceImpl作为前面流程分析的当前bean。
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'timeoutNotifyController': Injection of resource dependencies failed; nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'spuCheckDomainServiceImpl': Bean with name 'spuCheckDomainServiceImpl' has been injected into other beans [...] 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 'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.
然后提前我们在doCreateBean方法设置好spuCheckDomainServiceImpl加载时的条件断点。我们先debug启动异常的情况。最终断点信息如下:
从红框1里面的两个引用看,很明显调initializeBean方法时spring有对spuCheckDomainServiceImpl做代理增强。导致initializeBean后返回的引用和提前暴露到二级缓存的引用是不一致的。这里spuCheckDomainServiceImpl有二级缓存是跟我们前面分析的吻合,是因为spuCheckDomainServiceImpl被获取了两次,即调了两次doGetBean。
从红框2里面的actualDependentBeans的set集合知道提前暴露到二级缓存的引用有被其他33个bean引用到,也是跟异常提示的bean列表保持一致的。
这里spuCheckDomainServiceImpl的加载为什么会调用两次doGetBean方法呢?
从调用栈分析到该加载链如下:
TimeoutNotifyController ->spuCheckDomainServiceImpl-> newSpuApplyCheckServiceImpl-> ... ->spuCheckDomainServiceImplTimeoutNotifyController注入依赖时第一次调用doGetBean获取spuCheckDomainServiceImpl时,从一二三级缓存获取不到,会调用doCreateBean方法创建spuCheckDomainServiceImpl。
首先会将spuDomainServiceImpl放进spring的第三级缓存,然后开始调populateBean方法注入依赖,由于在循环中间的newSpuApplyCheckServiceImpl是第一次获取,一二三级缓存都获取不到,会调用doCreateBean去创建对应的bean,然后会第二次调用doGetBean获取spuCheckDomainServiceImpl,这时spuCheckDomainServiceImpl在第一次获取已经将bean加载到第三级缓存,所以这次spring会将bean从第三级缓存直接移到第二级缓存,并将第三级缓存里面的spuCheckDomainServiceImpl对应的bean删除,并直接返回二级缓存里面的bean,不会再调doCreateBean去创建spuCheckDomainServiceImpl。最终完成了循环中间的bean的初始化后(这里循环中间的bean初始化时依赖到的bean如果有引用到spuCheckDomainServiceImpl会调用doGetBean方法从二级缓存拿到spuCheckDomainServiceImpl提前暴露的引用),会回到第一次调用doGetBean获取spuCheckDomainServiceImpl时调用的doCreateBean方法的流程。继续调initializeBean方法完成初始化,然后将初始化完成的bean返回。最终拿初始化返回的bean引用跟二级缓存拿到的bean引用做对比,发现不一致,导致抛出UnsatisfiedDependencyException异常。
那么这里为什么spuCheckDomainServiceImpl调用initializeBean方法完成初始化后与提前暴露到二级缓存的bean会不一致呢?
看spuCheckDomainServiceImpl的代码如下:
@Component@Slf4j@Validatedpublic class SpuCheckDomainServiceImpl {@Resourceprivate NewSpuApplyCheckServiceImpl newSpuApplyCheckServiceImpl;}
发现SpuCheckDomainServiceImpl类有使用到@Validated注解。查阅资料发现@Validated的实现是通过在initializeBean方法里面执行一个org.springframework.validation.beanvalidation.MethodValidationPostProcessor后置处理器实现的,MethodValidationPostProcessor会对SpuCheckDomainServiceImpl做一层代理。导致initializeBean方法返回的spuCheckDomainServiceImpl是一个新的代理对象,从而最终导致跟二级缓存的不一致。
debug视图如下:
那为什么有时候能启动成功呢?什么情况下能启动成功?
我们继续debug启动成功的情况。最终观察到spuCheckDomainServiceImpl只会调用一次doGetBean,而且从一二级缓存拿到的spuCheckDomainServiceImpl提前暴露的引用为null,如下图:
这里为什么spuCheckDomainServiceImpl只会调用一次doGetBean呢?
首先我们根据调用栈整理到当前加载的引用栈:
oldCenterSpuController-> newSpuApplyCheckServiceImpl-> ... ->spuCheckDomainServiceImpl -> newSpuApplyCheckServiceImpl根据前面启动失败的信息我们可以知道,spuCheckDomainServiceImpl处理依赖的环是:
spuCheckDomainServiceImpl ->newSpuApplyCommandServiceImpl-> ... ->spuCheckDomainServiceImpl失败的情况我们发现是从spuCheckDomainServiceImpl开始创建的,现在启动正常的情况是从newSpuApplyCheckServiceImpl开始创建的。
创建 newSpuApplyCheckServiceImpl时,发现它依赖环中间这些bean会依次调用doCreateBean方法去创建对应的bean。
调用到spuCheckDomainServiceImpl时,由于是第一次获取bean,也会调用doCreateBean方法创建bean,然后回到创建spuCheckDomainServiceImpl的doCreateBean流程,这里由于没有将spuCheckDomainServiceImpl的三级缓存移到二级缓存,所以不会导致抛出UnsatisfiedDependencyException异常,最终回到newSpuApplyCheckServiceImpl的doCreateBean流程,由于newSpuApplyCheckServiceImpl在调用initializeBean方法没有做代理增强,所以也不会导致抛出UnsatisfiedDependencyException异常。因此最后可以正常启动。
这里我们会有疑问?类的创建顺序由什么决定的呢?
通常不同环境下,代码打包后的jar/war结构、@ComponentScan的basePackages配置细微差别,都可能导致Spring扫描和注册Bean定义的顺序不同。Java ClassLoader加载类的顺序本身也有一定不确定性。如果Bean定义是通过不同的配置类引入的,配置类的加载顺序会影响其中所定义Bean的注册顺序。
那是不是所有的类增强在有循环依赖时都会触发UnsatisfiedDependencyException异常呢?
并不是,比如@Transactional就不会导致触发UnsatisfiedDependencyException异常。让我们深入分析原因。
核心区别在于代理创建时机不同。
@Transactional的代理时机如下:
// Spring 为 @Transactional 创建代理的流程1. 实例化原始 Bean2. 放入三级缓存(ObjectFactory)3. 当发生循环依赖时,调用 ObjectFactory.getObject()4. 此时判断是否需要事务代理,如果需要则提前创建代理5. 将代理对象放入二级缓存,供其他 Bean 使用
@Validated的代理时机:
// @Validated 的代理创建在生命周期更晚的阶段1. 实例化原始 Bean2. 放入三级缓存(ObjectFactory)3. 当发生循环依赖时,调用 ObjectFactory.getObject()4. ❌ 问题:此时 @Validated 的代理还未创建!5. 其他 Bean 拿到的是原始对象,而不是异步代理对象
问题根源:@Transactional的代理增强是在三层缓存生成时触发的, @Validated的增强是在初始化bean后通过后置处理器做的代理增强。
解决方案
短期方案
移除SpuCheckDomainServiceImpl类上的Validated注解
@lazy 解耦
原理是发现有@lazy 注解的依赖为其生成代理类,依赖代理类,只有在真正需要用到对象时,再通过getBean的逻辑去获取对象,从而实现了解耦。
长期方案
严格执行DDD代码规范
这里是违反DDD分层规范导致的循环依赖。
梳理解决历史依赖环
通过梳理修改代码解决历史存在的依赖环。我们内部实现了一个能检测依赖环的工具,这里简单介绍一下实现思路,详情如下。
日常循环依赖环:实战检测工具类解析
在实际项目中,即使遵循了DDD分层规范和注入最佳实践,仍有可能因业务复杂或团队协作不充分而引入循环依赖。为了在开发阶段尽早发现这类问题,我们可以借助自定义的循环依赖检测工具类,在Spring容器启动后自动分析并报告依赖环。
功能概述:
条件启用:通过配置circular.dependecy.analysis.enabled=true开启检测;
依赖图构建:扫描所有单例Bean,分析其构造函数、字段、方法注入及depends-on声明的依赖;
循环检测算法:使用DFS遍历依赖图,识别所有循环依赖路径;
通知上报:检测结果通过飞书机器人发送至指定接收人(targetId)。
简洁代码结构如下:
@Component@ConditionalOnProperty(value = "circular.dependency.analysis.enabled", havingValue = "true")public class TimingCircularDependencyHandler extends AbstractNotifyHandler<NotifyData>implements ApplicationContextAware, BeanFactoryAware {@Overridepublic Boolean handler(NotifyData data) {dependencyGraph = new HashMap<>();handleContextRefresh(); // 触发依赖图构建与检测return Boolean.TRUE;}private void buildDependencyGraph() {// 遍历所有Bean,解析其依赖关系// 支持:构造器、字段、方法、depends-on}private void detectCircularDependencies() {// 使用DFS检测环,记录所有循环路径// 输出示例:循环依赖1: A -> B -> C -> A}}
四
总结
循环依赖暴露了代码结构的设计缺陷。理论上应通过分层和抽象来避免,但在复杂的业务交互中仍难以杜绝。虽然Spring利用三级缓存等机制默默解决了这一问题,使程序得以运行,但这绝不应是懈怠设计的借口。我们更应恪守设计原则,从源头规避循环依赖,构建清晰、健康的架构。
往期回顾
2. 从 JSON 字符串到 Java 对象:Fastjson 1.2.83 全程解析|得物技术
3. 用好 TTL Agent 不踩雷:避开内存泄露与CPU 100%两大核心坑|得物技术
文 /鲁班
关注得物技术,每周一、三更新技术干货
要是觉得文章对你有帮助的话,欢迎评论转发点赞~
未经得物技术许可严禁转载,否则依法追究法律责任。
“
扫码添加小助手微信
如有任何疑问,或想要了解更多技术资讯,请添加小助手微信: