zoukankan      html  css  js  c++  java
  • Bean的原始版本与最终版本不一致?记一次Spring IOC探索之旅

    前言

    在这个信息技术发展迅速的时代,万万没想到,Spring自2003年发展至今,仍是技术选型中的首选,某些项目甚至有Spring全家桶的情况。

    在Java开发者面试当中,Spring的原理也常被面试官用于考察候选人的技术深度,同时也能反映候选人对技术是否有热情,是否具有探索精神。

    本文带着一个开发中遇到的实际问题,对问题寻根问底,对Spring IOC进行一次探索之旅,其中会介绍到:

    1. 了解上述异常是什么,及其发生原理
    2. 了解Spring IOC中“获取Bean”、“创建Bean”的过程
    3. 了解@Async如何与Spring IOC协作实现异步方法的特性
    4. 了解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最后被包装的对象不一致。

    看着异常信息,心有疑问:

    1. 这个异常具体表示什么? 什么情况下会发生?
    2. 为什么会出现此异常?
    3. 这个异常为什么在开发者的机器上没出现,在测试环境却出现了?

    什么是BeanCurrentlyInCreationException

    BeanCurrentlyInCreationException,查看注释,可知这个异常是引用“正在创建中的Bean”时发生的异常,通常发生在“构造方法自动装配”匹配到“当前正在构造的Bean”的时候。

    这个异常有两个构造方法,其中一个构造方法可以自定义beanName和异常消息。我们遇到的这个异常信息,很明显是自定义的异常消息了,那这个异常消息具体表示什么呢?

    根据上述异常,简单翻译一下:

    创建名称为“appleService”的bean发生错误:因为循环引用,“appleService” bean的原始版本已注入到其它bean中(“boyService” bean),但“appleService” bean最终被包装了。
    这意味着其它bean不是使用“appleService” bean的最终版本。
    这通常是“急于进行类型匹配”的结果,比如可以考虑关闭“allowEagerInit”使用“getBeanNamesOfType”。

    从异常信息中,可以发现几个关键信息:

    1. 循环引用
    2. bean的原始版本
    3. 包装
    4. bean的最终版本

    通过上面几个信息,可以判断出很可能跟循环引用代理相关。

    1. 循环引用,很明显是Bean之前的循环引用
    2. 代理,是“代理模式”,代理模式在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.RELEASEJDK 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:

    1. 从缓存中获取Bean
    2. 从缓存没获取到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个层级的缓存:

    1. 第一级缓存是“已创建的单例缓存”(singletonObjects),这里很好理解。

    2. 第二级缓存是“提前暴露对象的缓存”(earlySingletonObjects),为了解决循环依赖,需要提前暴露没创建完毕的对象,所以有了此缓存的存在,也很好理解。

    3. 第三级缓存是“单例工厂缓存”(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个大的步骤:

    1. 实例化
    2. 解决循环依赖
    3. 填充属性值
    4. 调用初始化方法

    但上述4个大步骤中其实还有许多细节,进一步补充细节后,步骤如下:

    1. ★ 调用“实例化前扩展处理器”
    2. 实例化Bean
    3. ★ 为解决循环引用,这里判断是否需要提前暴露对象,如需,将“提前暴露引用工厂”放入“单例工厂缓存”
    4. ★ 处理实例化后扩展处理器
    5. 填充属性值
    6. ★ 处理初始化的前置处理
    7. 调用初始化方法
    8. ★ 初始化的后置处理
    9. ★ 检测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个,是“并且”的关系:

    1. 此bean是否定义为单例
    2. 全局配置是否允许循环引用
    3. 此单例是否正常创建中

    具体代码如下:

    如果需要提前暴露引用,会将获取提前暴露引用的方法封装成对象工厂(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要同时满足以下条件:

    1. Bean存在循环引用

    2. Bean提前暴露对象(即上文说了3个条件),并且将提前暴露的对象注入到其它Bean中

    3. 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生成代理对象。

    1. 实例化前扩展处理器

    2. 获取提前暴露的引用的扩展处理器

    3. 初始化后扩展处理器

    实例化前扩展处理器

    代码坐标: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要同时满足以下条件,结合“如何复现”章节的代码看是否满足:

    1. Bean存在循环引用(AppleService、BoyService循环引用,满足)

    2. Bean提前暴露对象(即上文说了3个条件),并且将提前暴露的对象注入到其它Bean中(AppleService、BoyService都会提前暴露引用,先加载的Bean会将提前暴露的对象注入到后加载的Bean中,所以,AppleService先加载满足,BoyService先加载不满足)

    3. Bean使用了一些特性,这些特性会在上面的扩展处理器替换Bean对象,导致Bean对象与最初的对象不一致(AppleService使用了@Async特性,会通过扩展处理器创建代理对象,AppleService满足)

    假如AppleService、BoyService循环引用,AppleService中包含@Async方法。

    假设先创建AppleService,再创建BoyService,会引发该异常。因为AppleService提前暴露了原始对象,并注入到BoyService的属性中,后来因为它有@Async,需要创建代理对象,最后发现原始对象与最终的代理对象不一致。具体见下图蓝色分支:

    假设先创建BoyService,再创建AppleService,不会引发该异常,过程跟先加载AppleService并无不同,不同点在于最后一个红色的节点,由于BoyService并无@Async(也就是先加载的Bean没使用创建代理的特性),所以不会创建代理对象,自然就不会引发“提前暴露的引用与最终的引用不一致”的异常。

    如何解决?

    我们知道了问题的原因,那解决方法自然手到擒来,有许多解决方法,比如:

    1. 将@Async方法提取到其它相关的Bean中,将@Async与循环引用分开
    2. 使用@Lazy等方式控制Bean的加载顺序,以避免具有@Async与循环引用的Bean先加载
    3. 用其它替代方式实现,比如使用@Async的,则用多线程方式处理

    解决方法不仅仅上面3种,还有很多很多,请自行挖掘。

    参考的优秀书籍与文章

    1. 书籍 - Spring源码深度解析

    2. 书籍 - Spring技术内幕 第2版

    3. 文章 - 跳出源码地狱,Spring巧用三级缓存解决循环依赖-原理篇

    最后

    小弟不才,学识有限,如有错漏,欢迎指正哈。
    如果本文对你有帮助,记得“一键三连”(“点赞”、“评论”、“收藏”)哦!

  • 相关阅读:
    70.BOM
    69.捕获错误try catch
    68.键盘事件
    523. Continuous Subarray Sum
    901. Online Stock Span
    547. Friend Circles
    162. Find Peak Element
    1008. Construct Binary Search Tree from Preorder Traversal
    889. Construct Binary Tree from Preorder and Postorder Traversal
    106. Construct Binary Tree from Inorder and Postorder Traversal
  • 原文地址:https://www.cnblogs.com/nick-huang/p/13929455.html
Copyright © 2011-2022 走看看