Bean的原始版本与最终版本不一致?记一次Spring IOC探索之旅
前言
在这个信息技术发展迅速的时代,万万没想到,Spring自2003年发展至今,仍是技术选型中的首选
,某些项目甚至有Spring全家桶
的情况。
在Java开发者面试当中,Spring的原理也常被面试官用于考察候选人的技术深度
,同时也能反映候选人对技术是否有热情
,是否具有探索精神。
本文带着一个开发中遇到的实际问题,对问题寻根问底
,对Spring IOC进行一次探索之旅,其中会介绍到:
- 了解上述异常是什么,及其发生原理
- 了解Spring IOC中“获取Bean”、“创建Bean”的过程
- 了解@Async如何与Spring IOC协作实现异步方法的特性
- 了解Spring AOP如何与Spring IOC协作实现“面向切面编程”的特性
阅读本文,你能够了解Spring IOC的基本原理。
收藏本文
,四舍五入你也是了解Spring原理的人啦。
问题背景
最近,在使用Spring过程中遇到一个问题:
开发同学开发完需求,并在开发环境完整地自测完毕,满怀自信地将其发布到测试环境,却发现测试环境连启动都起不起来,需求刚提测就被测试同学驳回。同样的代码回到开发人员的环境却又是正常的。
测试环境报的异常是这样的:
2020-08-01 09:54:48.490 ERROR 628 --- [ restartedMain] o.s.boot.SpringApplication : Application run failed
org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'appleService': Bean with name 'appleService' has been injected into other beans [boyService] 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.
简单翻译,大概讲在循环引用情况下,一个Bean注入到其它Bean后被包装了,导致注入到其它Bean的对象与该Bean最后被包装的对象不一致。
看着异常信息,心有疑问:
- 这个异常具体表示什么? 什么情况下会发生?
- 为什么会出现此异常?
- 这个异常为什么在开发者的机器上没出现,在测试环境却出现了?
什么是BeanCurrentlyInCreationException
BeanCurrentlyInCreationException,查看注释,可知这个异常是引用“正在创建中的Bean”时发生的异常,通常发生在“构造方法自动装配”匹配到“当前正在构造的Bean”的时候。
这个异常有两个构造方法,其中一个构造方法可以自定义beanName和异常消息。我们遇到的这个异常信息,很明显是自定义的异常消息了,那这个异常消息具体表示什么呢?
根据上述异常,简单翻译一下:
创建名称为“appleService”的bean发生错误:因为循环引用,“appleService” bean的原始版本已注入到其它bean中(“boyService” bean),但“appleService” bean最终被包装了。
这意味着其它bean不是使用“appleService” bean的最终版本。
这通常是“急于进行类型匹配”的结果,比如可以考虑关闭“allowEagerInit”使用“getBeanNamesOfType”。
从异常信息中,可以发现几个关键信息:
- 循环引用
- bean的原始版本
- 包装
- bean的最终版本
通过上面几个信息,可以判断出很可能跟循环引用
和代理
相关。
- 循环引用,很明显是Bean之前的循环引用
- 代理,是“代理模式”,代理模式在Spring中很常用,比如AOP、@Transactional、@Asnyc都用到了
于是,我们开始检查代码,看报错信息中涉及的代码是否有蛛丝马迹。
结果发现相关Bean确实存在循环引用,并且部分Bean的方法有使用异步注解@Asnyc。
“循环引用”和“@Asnyc”都是Spring比较常用的功能,究竟是不是它们引发这个异常呢?
如何复现?
根据发生异常的Bean的写法,撇除其它干扰因素,用独立的简单应用复现。
先准备最简单的Spring Boot脚手架,这里使用的版本是2.2.2.RELEASE:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.2.RELEASE</version>
<relativePath/>
</parent>
添加两个bean,分别是AppleService和BoyService,它们包含两个特征:
1、循环引用。AppleService包含BoyService类型的属性,BoyService也包含AppleService类型的属性,它们互相引用
2、异步方法。AppleService中包含一个异步方法,用@Async注解实现
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
@Service
public class AppleService {
@Autowired
private BoyService boyService;
@Async
public String color() {
return "red";
}
}
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class BoyService {
@Autowired
private AppleService appleService;
public String color() {
return "white";
}
}
然后在Spring Boot的启动类中添加支持异步注解的注解:@EnableAsync
执行Spring Boot的启动类,就会看到以下报错日志(这是在Windows操作系统下运行的结果,在其他操作系统下运行有可能是正常的):
2020-10-23 00:55:33.878 ERROR 2348 --- [ restartedMain] o.s.boot.SpringApplication : Application run failed
org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'appleService': Bean with name 'appleService' has been injected into other beans [boyService] 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.
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:624) ~[spring-beans-5.2.2.RELEASE.jar:5.2.2.RELEASE]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:517) ~[spring-beans-5.2.2.RELEASE.jar:5.2.2.RELEASE]
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:323) ~[spring-beans-5.2.2.RELEASE.jar:5.2.2.RELEASE]
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:222) ~[spring-beans-5.2.2.RELEASE.jar:5.2.2.RELEASE]
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:321) ~[spring-beans-5.2.2.RELEASE.jar:5.2.2.RELEASE]
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202) ~[spring-beans-5.2.2.RELEASE.jar:5.2.2.RELEASE]
at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:879) ~[spring-beans-5.2.2.RELEASE.jar:5.2.2.RELEASE]
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:878) ~[spring-context-5.2.2.RELEASE.jar:5.2.2.RELEASE]
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:550) ~[spring-context-5.2.2.RELEASE.jar:5.2.2.RELEASE]
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:141) ~[spring-boot-2.2.2.RELEASE.jar:2.2.2.RELEASE]
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:747) [spring-boot-2.2.2.RELEASE.jar:2.2.2.RELEASE]
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:397) [spring-boot-2.2.2.RELEASE.jar:2.2.2.RELEASE]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:315) [spring-boot-2.2.2.RELEASE.jar:2.2.2.RELEASE]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1226) [spring-boot-2.2.2.RELEASE.jar:2.2.2.RELEASE]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1215) [spring-boot-2.2.2.RELEASE.jar:2.2.2.RELEASE]
at com.nickxhuang.springbootexercise.SpringbootexerciseApplication.main(SpringbootexerciseApplication.java:14) [classes/:na]
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_191]
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_191]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_191]
at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_191]
at org.springframework.boot.devtools.restart.RestartLauncher.run(RestartLauncher.java:49) [spring-boot-devtools-2.2.2.RELEASE.jar:2.2.2.RELEASE]
跟着上述堆栈,我们开始探索为什么会引发此异常。
代码版本说明
如无特别说明,下文使用的版本是Spring Boot 2.2.2.RELEASE
、JDK 1.8
:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.2.RELEASE</version>
<relativePath/>
</parent>
Bean的循环引用是什么?
在上述复现异常的章节中,我们展示了循环引用的类的代码,用类图表示是这样的:
当然,两个Bean组成的循环引用是最简单的情况,更多的情况下,是3个或3个以上的Bean组成的循环引用:
随着业务发展,应用会越来越复杂,Bean也会越来越多,循环引用在很多应用中不可避免会存在。广泛使用的Spring支持循环引用吗?
Spring如何创建循环引用的Bean?
原型模式下不允许循环依赖的Bean,为什么?
Spring Bean的模式,常用的有单例模式和原型模式(Prototype模式)。当定义Bean为原型模式时,Bean就是多实例的,每次调用方法获取Bean时,都会新建一个Bean实例。
为什么原型模式下不允许循环依赖?
因为原型模式下,每获取一次Bean,都会新建一个该Bean的新实例,如果遇到循环依赖的情况,就会出现死循环,比如:
appleService > boyService > appleService > boyService > appleService 如此不断循环
所以,Spring在创建Bean的过程中使用校验禁止了这种情况的发生,代码坐标:org.springframework.beans.factory.support.AbstractBeanFactory#doGetBean
的264-268行。
其原理是使用一个变量缓存“多实例模式下正在创建的Bean的名字”,以此缓存来判断是否存在已在创建中的Bean再进行创建动作,如有则抛出异常。
单例模式支持解决循环引用?有什么前提条件吗?
“单例模式”下基于“Setter方式注入属性”支持循环引用
在项目开发中,Bean随着业务的复杂越来越多,循环引用,在使用中似乎难以避免。
而循环引用,真的无法解决吗?细想好像不是的。
创建一个Bean,大致能分为两步:实例化和填充值。如果我们把实例化完但还没填充值得Bean缓存起来,在填充值的时候想办法从缓存中获取依赖Bean的引用,这样似乎可行。
比如AppleService、BoyService循环引用,那么创建过程可以是这样的:
在绿色方框获取AppleService的对象时,将我们在蓝色方框步骤缓存起来的AppleService拿出来,这时的AppleService应该是已实例化但还未完成创建完成的。
没错,Spring就是用这个原理支持“单例模式”下基于“Setter方式注入属性”的循环引用的Bean。
实例化Bean,并将未完全创建完毕的Bean缓存起来,这就是上文异常信息中说的“EagerInit”(急切的初始化),也是下文所说的“提前暴露对象”。
本节描述的创建Bean的过程是经过抽象的,因为要兼容AOP等其他特性的扩展,Spring创建Bean的实现比上面描述的要复杂很多。
Spring获取Bean的过程是怎样的?
如何快速地找到Spring获取bean的入口?
从调用堆栈中寻找是最快的,在上述“如何复现?”章节中有一个堆栈信息,观察方法名很容易能发现端倪,我们节选了堆栈中的一段:
org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:624) ~[spring-beans-5.2.2.RELEASE.jar:5.2.2.RELEASE]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:517) ~[spring-beans-5.2.2.RELEASE.jar:5.2.2.RELEASE]
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:323) ~[spring-beans-5.2.2.RELEASE.jar:5.2.2.RELEASE]
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:222) ~[spring-beans-5.2.2.RELEASE.jar:5.2.2.RELEASE]
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:321) ~[spring-beans-5.2.2.RELEASE.jar:5.2.2.RELEASE]
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
堆栈中的方法名要么是getBean,要么是createBean,我们从最上层的org.springframework.beans.factory.support.AbstractBeanFactory.getBean开始查看。
Spring如何获取Bean?
getBean的方法的具体坐标是org.springframework.beans.factory.support.AbstractBeanFactory#getBean(java.lang.String)
方法。
查看其中,可发现实际调用的是org.springframework.beans.factory.support.AbstractBeanFactory#doGetBean
方法。
doGetBean方法是获取Bean的实际实现方法,这个方法有挺多细节,从顶层抽象视角来看,它通过两种方式获取Bean:
- 从缓存中获取Bean
- 从缓存没获取到Bean,需创建Bean
除了上述两个大的抽象步骤,还有许多细节,比如在“从缓存中获取Bean”之后、“调用创建Bean的方法”之前,有几个检测、委托获取的步骤。
下图是逻辑流程图,图中标注了该逻辑的具体实现代码行数,可以对照着看(代码的版本查看“代码版本说明”章节):
1、从缓存中获取Bean。Spring的Bean常用有的单例模式(默认)和原型模式(多实例模式)。
单例模式下,如果一个Bean之前就创建过,那当然已经缓存起来了,第2+次获取直接从缓存中获取就好了。
上述从缓存中获取已经创建好的Bean是最简单的情况,为支持循环引用,Spring用3个级别的缓存来获取尚未完全创建完的Bean,下面会详细讨论。
2、检测“原型模式”下的Bean是否在创建中。为什么呢?
从上面的介绍中,我们知道因为死循环的原因,“原型模式”下是不允许循环引用的,所以这里对“原型模式下的Bean是否在创建中”进行校验,如果当前需要创建的Bean已经在创建中了,说明存在循环引用,会抛出异常。
这里用一个ThreadLocal类型的变量做缓存,存放正在创建中的Bean名称,通过此缓存来判断对应的Bean是否正在创建中的。
具体判断细节可见代码org.springframework.beans.factory.support.AbstractBeanFactory#isPrototypeCurrentlyInCreation
,这里不做赘述。
维护此缓存的代码为org.springframework.beans.factory.support.AbstractBeanFactory#beforePrototypeCreation
,ThreadLocal类型的缓存在只存一个Bean的时候存放的是一个字符创,如果存在多个Bean时,存放的则是Set类型(包含多个字符串)。
3、如果存在“父BeanFactory”,且beanName未定义在本BeanFactory中,调用“父BeanFactory”获取Bean
4、如果存在“depends on属性”依赖的Bean,先加载依赖的Bean。
5、创建Bean。创建的Bean可以分为3种模式,分别是单例模式、原型模式、其它Scope模式。
由于我们是排查BeanCurrentlyInCreationException问题,所以我们下面从单例模式这一条线介绍如何创建Bean。(单例模式也是默认的模式,是大家最常用的模式)
从缓存中获取Bean?为什么需要三级缓存?
获取Bean方法的第一步,是从缓存中获取Bean。
从缓存中获取Bean,代码坐标是org.springframework.beans.factory.support.DefaultSingletonBeanRegistry#getSingleton(java.lang.String)
,具体业务逻辑如下图:
可见分了3个层级的缓存:
-
第一级缓存是“已创建的单例缓存”(singletonObjects),这里很好理解。
-
第二级缓存是“提前暴露对象的缓存”(earlySingletonObjects),为了解决循环依赖,需要提前暴露没创建完毕的对象,所以有了此缓存的存在,也很好理解。
-
第三级缓存是“单例工厂缓存”(singletonFactories)。提前暴露对象,看起来通过第二级缓存就能解决了,为什么还需要第三级缓存呢?
我们先看singletonFactories是哪里维护的:
可以发现有一个可疑的维护点:org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean
,这个方法的579-589行是创建Bean时提前暴露对象的实现点,如果计算所得的earlySingletonExposure为true表示需要提前暴露对象,则将“提前暴露对象工厂”放入“单例工厂缓存”中。
跟踪进去查看“提前暴露对象工厂”封装的“获取提前暴露对象引用的方法”(org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#getEarlyBeanReference
),可以发现里面调用一系列SmartInstantiationAwareBeanPostProcessor扩展处理器,这些扩展处理的返回结果赋予exposedObject从而替换原来的Bean。
可见,为了支持这些扩展处理器,使用第三级缓存来存放单例工厂,到真正需要获取“提前暴露对象”时,才调用工厂方法获取“提前暴露对象”,触发调用扩展处理器。
另外,这里是Spring IOC与Spring AOP协作的一个点,Spring IOC在这里会调用一个Spring AOP实现的SmartInstantiationAwareBeanPostProcessor扩展处理器,处理器会返回创建好的Bean的代理对象,替换原来Bean对象。
Spring如何创建Bean?
查看org.springframework.beans.factory.support.AbstractBeanFactory#doGetBean
的319-373行,就是创建Bean的逻辑了,代码篇幅太长,就不贴了。
可以发现,无论单例模式还是原型模式、其它模式,都是使用这个方法创建Bean的:org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#createBean(java.lang.String, org.springframework.beans.factory.support.RootBeanDefinition, java.lang.Object[])
。
回调方式 + 模板方式模式,复用“创建Bean的逻辑”
单例模式创建Bean这里用得很巧妙,用了“简单工厂模式”对此方法进行封装,然后传入org.springframework.beans.factory.support.DefaultSingletonBeanRegistry#getSingleton(java.lang.String, org.springframework.beans.factory.ObjectFactory<?>)
进行调用。
查看getSingleton的逻辑,会发现逻辑主要是维护各个缓存。这种实现方式是不是似曾相识,像不像模板方法模式?我们经常通过继承抽象类的方式实现模板方法模式,而这里使用回调方式来实现模板方式模式,从而复用“创建Bean的逻辑”,“缓存维护逻辑”则封装在模板方法中,干得漂亮。
创建Bean的过程是怎么样的?
接下来看创建Bean的过程是怎么样的?
从大的步骤来讲,创建Bean会经历下面4个大的步骤:
- 实例化
- 解决循环依赖
- 填充属性值
- 调用初始化方法
但上述4个大步骤中其实还有许多细节,进一步补充细节后,步骤如下:
- ★ 调用“实例化前扩展处理器”
- 实例化Bean
- ★ 为解决循环引用,这里判断是否需要提前暴露对象,如需,将“提前暴露引用工厂”放入“单例工厂缓存”
- ★ 处理实例化后扩展处理器
- 填充属性值
- ★ 处理初始化的前置处理
- 调用初始化方法
- ★ 初始化的后置处理
- ★ 检测Bean的原始版本与最终版本是否一致
其中带★号的点,与本文讨论话题关联较大,所以下文会分点介绍。
下图是逻辑流程图,图中标注了该逻辑的具体实现代码行数,可以对照着看(代码的版本查看“代码版本说明”章节):
调用「实例化前扩展处理器」
代码坐标:org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#createBean(java.lang.String, org.springframework.beans.factory.support.RootBeanDefinition, java.lang.Object[])
的504-514行,是处理“实例化前扩展处理器”的代码。
如果返回的bean对象不为null,则直接返回,不走后面的创建Bean的工作了,这是一个截断的动作。也就是说如果有“实例化扩展处理器”的方法返回的Bean不为null,则使用这个返回的Bean,后面的创建Bean的动作被省略了。
这里有个知识点,Spring IOC与Spring AOP协作的其中一个地方就是这里:
我们用最简单的Spring Boot脚手架加上AOP特性调试,就能发现有个实例化前扩展处理器叫org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator
,这是Spring IOC与Spring AOP协作的一个地方,下文“Spring IOC与Spring AOP如何协作对Bean生成代理”会详细介绍。
为解决循环引用,提前暴露引用
为解决循环依赖,这里会判断是否需要提前暴露引用,如需,将“提前暴露引用的方法”封装成工厂对象,放入“单例工厂缓存”。坐标:org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean
中583-589行。
“是否需要提前暴露引用”的判断条件有3个,是“并且”的关系:
- 此bean是否定义为单例
- 全局配置是否允许循环引用
- 此单例是否正常创建中
具体代码如下:
如果需要提前暴露引用,会将获取提前暴露引用的方法封装成对象工厂(ObjectFactory),以beanName为键放入“单例工厂缓存”中。“单例工厂缓存”则会在上述的获取缓存方法中使用到,具体是在第3级缓存中使用到。
具体代码如下:
我们需要继续看getEarlyBeanReference方法:
可以发现里面调用一系列SmartInstantiationAwareBeanPostProcessor扩展处理器,这些扩展处理的返回结果赋予exposedObject从而替换原来的Bean。
与本文讨论话题相关的扩展处理器有1个:
org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator
,IOC与AOP连接的地方,详见下文“Spring IOC与Spring AOP如何协作对Bean生成代理”章节。
调用“初始化后扩展处理器”
坐标:org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#initializeBean(java.lang.String, java.lang.Object, org.springframework.beans.factory.support.RootBeanDefinition)
中1799-1801行。
可以通过扩展处理器对Bean进行加工。
与本文讨论话题相关的处理器有1个:
org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator
,IOC与AOP连接的地方,详见下文“Spring IOC与Spring AOP如何协作对Bean生成代理”章节。
检测Bean的原始版本与最终版本是否不一致
查看org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean
的607-632行,可以知道触发BeanCurrentlyInCreationException要同时满足以下条件:
-
Bean存在循环引用
-
Bean提前暴露对象(即上文说了3个条件),并且将提前暴露的对象注入到其它Bean中
-
Bean使用了一些特性,这些特性会在上面的扩展处理器替换Bean对象,导致Bean对象与最初的对象不一致
用本文的案例遇到的异常来分析:
我们的代码中同时存在循环依赖和@Async,通过查看“Spring IOC与@Async如何协作对Bean生成代理”章节,可以发现@Async的实现实际上是在“初始化后扩展处理器”中对Bean进行包装代理。
结合循环依赖的Bean需要提前暴露对象,就造成了提前暴露对象时暴露的是Bean的原始版本,而@Async的实现对Bean进行代理包装后的是最终版本,所以Bean的最终版本不等于原始版本,就触发了上述异常,导致应用启动不起来了。
Spring IOC如何跟其它特性协作?
Spring IOC与@Async如何协作对Bean生成代理?
如果Spring IOC管理的Bean使用@Async实现异步调用,Spring是如何为相关Bean生成代理对象的呢?
通过调试代码,观察Bean在哪个节点变化成代理对象,发现下述地方会对Bean生成代理对象:初始化后扩展处理器。
@Async的初始化后扩展处理器的实现类是:org.springframework.scheduling.annotation.AsyncAnnotationBeanPostProcessor
,实际方法是由父类实现的:org.springframework.aop.framework.AbstractAdvisingBeanPostProcessor#postProcessAfterInitialization
。
查看该方法,可以看到如果满足条件,最后会触发AbstractAdvisingBeanPostProcessor类92行的方法创建代理对象:
继续跟踪进去,来到org.springframework.aop.framework.DefaultAopProxyFactory#createAopProxy
,可以发现创建代理的方式有两种,如果有实现接口,则使用JDK动态代理的方式(JdkDynamicAopProxy),否则使用CGLIB(ObjenesisCglibAopProxy)。
有个疑问,既然循环依赖和@Async会引发偶现的BeanCurrentlyInCreationException,而AOP与@Async底层都是依赖代理,循环依赖和AOP同时使用的情况下会有同样的问题吗?
后续章节我们会对AOP进行讨论,@Transactional是否有对循环依赖的情况做支持呢?请自行探究哈。
Spring IOC与Spring AOP如何协作对Bean生成代理?
如果Spring IOC维护的Bean涉及面向切面编程,需要Spring AOP为之生成代理对象,那么Spring IOC和Spring AOP是在哪里协作的呢?
通过调试代码,观察Bean在哪里产生代理对象,发现下述3处地方有可能会对Bean生成代理对象。
-
实例化前扩展处理器
-
获取提前暴露的引用的扩展处理器
-
初始化后扩展处理器
实例化前扩展处理器
代码坐标:org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator#postProcessBeforeInstantiation
这里是实例化的前置处理,也就是说目标对象还没有实例化,那么有个疑问,如何对还未实例化的对象进行代理?
阅读如下代码可知,这里是处理配置了customTargetSourceCreators
代理的地方,这个特性用得貌似不多,反正我没使用过。也就是说,如果没配置customTargetSourceCreators
,并不是在这里创建代理:
获取提前暴露的引用的扩展处理器
代码坐标:org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator#getEarlyBeanReference
这里是提前暴露引用的扩展处理器,如上文描述,多个单例Bean存在循环依赖的情况下,创建这些Bean的时候会提前暴露引用,提前暴露引用前会处理“获取提前暴露的引用的扩展处理器”,这是其中一个,用于处理提前暴露引用的Bean需要进行AOP处理的情况。
初始化后扩展处理器
代码坐标:org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator#postProcessAfterInitialization
这里是常规的生成代理对象的地方,生成代理对象是使用wrapIfNecessary方法:
Spring AOP是否兼容提前暴露对象?
查看了这3个扩展处理器,其中包含“获取提前暴露的引用的扩展处理器”。并且在3个扩展处理器中均会获取cacheKey,然后根据cacheKey来判断是否已经处理,如处理了就不重复处理了,所以Spring AOP是兼容提前暴露对象的:
为什么启动失败是偶现的?因为不同环境下Bean加载顺序不一致
因为加载Bean的顺序取决于从文件系统中获取Bean的顺序,而从文件系统获取Bean文件是通过java.io.File#listFiles()方法获取一个文件夹下的文件列表的,查看这个方法的注释可以发现它是不保证顺序的。
而Spring通过java.io.File#listFiles()获取需加载的Bean文件列表后,会对文件进行重新排序,这个重新排序旧版本与新版本的实现有些不一样,我们下面介绍3个版本,它们的实现方式也是不断地迭代优化中。代码坐标为org.springframework.core.io.support.PathMatchingResourcePatternResolver#doRetrieveMatchingFiles
。
spring-core-5.1.9的Bean加载顺序
在spring-core-5.1.9(spring-boot-starter-parent 2.2.2.RELEASE)中,可以发现获取一个目录的文件列表已经封装了一个叫listDirectory的方法,在此方法里依赖自定义的文件名对比器进行排序:
spring-core-4.3.12的Bean加载顺序
而在spring-core-4.3.12(spring-boot-starter-parent 1.5.8.RELEASE)中,则通过数组工具类的方式使用File默认的java.io.File#compareTo进行排序:
spring-core-4.2.8的Bean加载顺序
而在spring-core-4.2.8(spring-boot-starter-parent 1.3.8.RELEASE)中,使用java.io.File#listFiles()获取到文件列表后,没对文件列表进行排序,然后就开始便利文件列表继续递归调用。
会有哪些问题呢?
spring-core-4.2.8的Bean加载顺序,会有哪些问题呢?
1、这样有可能导致不同操作系统加载Bean的顺序是不一致的,比如使用此版本的Spring,同样的代码在Windows加载Bean的顺序跟在Linux很可能不一致。
如何验证?这个场景比较容易复现,在Spring脚手架中定义多个Bean,然后在各个Bean的默认构造方法打印一下日志,分别在Windows和Linux环境中启动,然后观察各个Bean默认构造方法的执行顺序即可。
2、甚至同一操作系统在多次不同的启动时可能不一致?因为java.io.File#listFiles()返回的文件列表是不保证顺序的,它依赖与各操作系统的JDK的逻辑以及各操作系统的底层实现。我们跟踪java.io.File#listFiles(),就能发现它依赖的是java.io.FileSystem#list,我看的Windows的JDK源码,这里使用的是WinNTFileSystem:
为什么Bean加载顺序不一致会导致有时成功,有时失败呢?
结合上面介绍“提前暴露对象”和“检测Bean的原始版本与最终版本是否不一致”的内容,可以知道触发BeanCurrentlyInCreationException要同时满足以下条件,结合“如何复现”章节的代码看是否满足:
-
Bean存在循环引用(AppleService、BoyService循环引用,满足)
-
Bean提前暴露对象(即上文说了3个条件),并且将提前暴露的对象注入到其它Bean中(AppleService、BoyService都会提前暴露引用,先加载的Bean会将提前暴露的对象注入到后加载的Bean中,所以,AppleService先加载满足,BoyService先加载不满足)
-
Bean使用了一些特性,这些特性会在上面的扩展处理器替换Bean对象,导致Bean对象与最初的对象不一致(AppleService使用了@Async特性,会通过扩展处理器创建代理对象,AppleService满足)
假如AppleService、BoyService循环引用,AppleService中包含@Async方法。
假设先创建AppleService,再创建BoyService,会引发该异常。因为AppleService提前暴露了原始对象,并注入到BoyService的属性中,后来因为它有@Async,需要创建代理对象,最后发现原始对象与最终的代理对象不一致。具体见下图蓝色分支:
假设先创建BoyService,再创建AppleService,不会引发该异常,过程跟先加载AppleService并无不同,不同点在于最后一个红色的节点,由于BoyService并无@Async(也就是先加载的Bean没使用创建代理的特性),所以不会创建代理对象,自然就不会引发“提前暴露的引用与最终的引用不一致”的异常。
如何解决?
我们知道了问题的原因,那解决方法自然手到擒来,有许多解决方法,比如:
- 将@Async方法提取到其它相关的Bean中,将@Async与循环引用分开
- 使用@Lazy等方式控制Bean的加载顺序,以避免具有@Async与循环引用的Bean先加载
- 用其它替代方式实现,比如使用@Async的,则用多线程方式处理
解决方法不仅仅上面3种,还有很多很多,请自行挖掘。
参考的优秀书籍与文章
-
书籍 - Spring源码深度解析
-
书籍 - Spring技术内幕 第2版
-
文章 - 跳出源码地狱,Spring巧用三级缓存解决循环依赖-原理篇
最后
小弟不才,学识有限,如有错漏,欢迎指正哈。
如果本文对你有帮助,记得“一键三连
”(“点赞
”、“评论
”、“收藏
”)哦!
本博客为学习、笔记之用,以笔记形式记录学习的知识与感悟。学习过程中可能参考各种资料,如觉文中表述过分引用,请务必告知,以便迅速处理。如有错漏,不吝赐教。
如果本文对您有用,点赞或评论哦;如果您喜欢我的文章,请点击关注我哦~