背景
大家都知道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
有三个条件:
- 条件一:
earlySingletonExposure=true
运行提前暴露的单例Bean,因为Bean默认都是单例的,所以这个一般都会满足。 - 条件二:
earlySingletonReference != null
在提前暴露的二级缓存中存在,出现场景:假设ABean在创建的时候,其他BBean在getBean(ABean)
,此时二级缓存中就会不为空。 - 条件三:
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)
总结一下条件就是:
- advisor instanceof PointcutAdvisor
- 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比较的条件。