Spring属性间循环依赖抛BeanCurrentlyInCreationException

Spring属性间循环依赖抛BeanCurrentlyInCreationException

背景

大家都知道Spring解决不了构造之间的循环依赖,但是可以解决属性间的循环依赖。
举个简单例子:

@Service
public class AAService {
	@Autowired
    private BBService bbService;
    void printAA(String value){
        System.out.println("AA print value: "+ value);
    }
}

@Service
public class BBService {
	@Autowired
    private AAService aaService;
    void printBB(String value){
        System.out.println("AA print value: "+ value);
    }
}

上面的例子就是属性间的循环依赖,这个启动是没有任何问题的。

Spring为了解决上面场景的解决方案是依赖于DefaultSingletonBeanRegistry的三级缓存,也就是三个Map对象:

	/** Cache of singleton objects: bean name to bean instance. */
	private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

	/** Cache of singleton factories: bean name to ObjectFactory. */
	private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

	/** Cache of early singleton objects: bean name to bean instance. */
	private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);

具体实现步骤可以看下图:
在这里插入图片描述

场景重现

但是最近的我在做项目代码迁移的时候发现启动容器会抛出如下异常:

org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'AAServiceImpl': Bean with name 'AAServiceImpl' has been injected into other beans [BBServiceImpl] 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:631) ~[spring-beans-5.3.9.jar:5.3.9]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:524) ~[spring-beans-5.3.9.jar:5.3.9]
	at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335) ~[spring-beans-5.3.9.jar:5.3.9]
	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-5.3.9.jar:5.3.9]
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333) ~[spring-beans-5.3.9.jar:5.3.9]
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208) ~[spring-beans-5.3.9.jar:5.3.9]
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:944) ~[spring-beans-5.3.9.jar:5.3.9]
	at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:918) ~[spring-context-5.3.9.jar:5.3.9]
	at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:583) ~[spring-context-5.3.9.jar:5.3.9]
	at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:145) ~[spring-boot-2.5.3.jar:2.5.3]
	at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:754) [spring-boot-2.5.3.jar:2.5.3]
	at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:434) [spring-boot-2.5.3.jar:2.5.3]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:338) [spring-boot-2.5.3.jar:2.5.3]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1343) [spring-boot-2.5.3.jar:2.5.3]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1332) [spring-boot-2.5.3.jar:2.5.3]
	at com.example.demo.DemoApplication.main(DemoApplication.java:18) [classes/:na]
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_271]
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_271]
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_271]
	at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_271]
	at org.springframework.boot.devtools.restart.RestartLauncher.run(RestartLauncher.java:49) [spring-boot-devtools-2.5.3.jar:2.5.3]

看异常信息知道是触发了循环依赖,导致容器启动失败。但是这和我们上面看到的Spring可以解决属性间的循环依赖结论明显不一样。最后仔细查看代码发现在AAService多了一个@Validated注解,尝试将这个注解删掉之后启动容器成功。

瞬间像是找到了Spring的Bug一样兴奋,然后就开始看异常栈和debug排查原因。

问题分析

从异常栈中可以看到抛异常的地方在AbstractAutowireCapableBeanFactory#doCreateBean()方法中,下面是简化后的doCreateBean()代码:
在这里插入图片描述
能抛出BeanCurrentlyInCreationException有三个条件:

  1. 条件一:earlySingletonExposure=true运行提前暴露的单例Bean,因为Bean默认都是单例的,所以这个一般都会满足。
  2. 条件二:earlySingletonReference != null在提前暴露的二级缓存中存在,出现场景:假设ABean在创建的时候,其他BBean在getBean(ABean),此时二级缓存中就会不为空。
  3. 条件三:exposedObject != bean,也就是实例化后的Bean与初始化完成的Bean不相等,添加@Validated后这里确实这里确实不相等,所以走到else if中。

所以下面就看一下为什么初始化之后exposedObect被改变了?
在这里插入图片描述

debug之后发现,是在循环执行BeanPostProcessor#postProcessAfterInitialization时,执行到了AbstractAdvisingBeanPostProcessor#postProcessAfterInitialization(),这个方法中会创建一个代理对象并返回,也就找到了Bean被改变的地方,下面就看下Bean被改变的条件:
在这里插入图片描述

就是因为isEligible()=True,导致Bean被代理待续覆盖。下面就看一下它的判断条件:
跟踪链路:

AbstractBeanFactoryAwareAdvisingPostProcessor#isEligible
AbstractAdvisingBeanPostProcessor#isEligible(java.lang.Class<?>)
AopUtils#canApply(org.springframework.aop.Advisor, java.lang.Class<?>, boolean)
AopUtils#canApply(org.springframework.aop.Pointcut, java.lang.Class<?>, boolean)

总结一下条件就是:

  1. advisor instanceof PointcutAdvisor
  2. advisor.getMethodMatcher() = MethodMatcher.TRUE

但是这些值是从哪里来的呢?看Validated的源码发现上面有说明:
在这里插入图片描述

看一下MethodValidationPostProcessor发现它实现了InitializingBean接口,所以在初始化时会执行afterPropertiesSet()
在这里插入图片描述
afterPropertiesSet()中 new 一个 AnnotationMatchingPointcut。它的构造中就设置了上面的值:
在这里插入图片描述

上面就找到了exposedObject != bean的原因。但是从Spring的@Validated示例来看确实可以这用,用了又会启动失败。后来我又把@Validated换成了@Async,结果也会出现上面的问题。所以在gitHub上提了一个issue,有人给了解决方案,感兴趣的可以看一看:github issue地址,最终被归到:Circular dependency + setter injection + Java Config => exception

上面给出的解决方案是在BService依赖Aservice上添加@Lazy。我试了一下确实可以,于是我有又分析了为什么加一个@Lazy就可以不抛异常。

经过debug后发现在属性依赖时,在进行属性注入的时候会先判断:isLazy(),只有isLazy()=false时才会调getBean(AService)从三级缓存中获取factory,提前暴露AService并放入二级缓存。

既然加了@Lazy不会将AService放入二级缓存,那上面的条件二earlySingletonReference != null就不会成立。也就跳过了条件三:exposedObject != bean的比较。

代码如下:DefaultListableBeanFactory#resolveDependency
在这里插入图片描述

具体的逻辑如下图:
在这里插入图片描述

总结

如果Bean之间存在属性间的循环依赖,并且其中一个Bean又添加了@Validated或者@Async这样的注解,就会导致上面的问题发生。而使用@Lazy可以解决,原因是没有将提前暴露的bean放入二级缓存,从而跳过了后面bean比较的条件。

  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值