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巧用三级缓存解决循环依赖-原理篇

最后

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

posted @ 2020-11-05 23:41  nick_huang  阅读(1758)  评论(1编辑  收藏  举报