一万字深度剖析Spring循环依赖

开篇词

距离上一篇文章更新已经有一年多时间了,之前写的大都是偏向于基础性的知识,也没有掺杂过多个人的思考。而自己一直以来都想写一些更加有深度的内容,这篇文章就是基于这样一个想法的尝试,希望读者能够有更多的收获。

为什么要读源码?

源码的学习其实是一个非常枯燥的过程,也是一个难以持之以恒的事情。然而越是难的事情,越是能让人成长。在此之前我一直没有勇气深入阅读和研究源码,觉得以自己目前一两年的经验去接触源码为时过早,相信很多小伙伴也有同样的想法。然而事实却并非如此,这只是我们内心惧怕源码而找的一种借口罢了,源码远没有大家想的那么可怕。

那么读源码究竟有什么好处呢?

  1. 可以帮助你更深刻地理解内部设计原理,提升你的系统架构能力和代码功力
  2. 可以帮你快速定位问题并制定调优方案,减少解决问题的时间成本
  3. 如果你足够优秀,你还能参加技术开源社区,成为一名代码贡献者
  4. 大厂面试的“加分项”甚至是“必选项”

今天我以这篇文章带你一起走进源码的世界,初步感受探索源码的乐趣。在java面试中一道常被问到的经典Spring面试题:Spring究竟是如何解决的循环依赖?相信很多看到相关博客、文章的小伙伴都能从容的给出答案:Spring通过提前曝光机制,使用三级缓存解决了循环依赖。

那么仅仅知道这个就足够了吗?为了了解你对Spring框架的掌握程序,聪明的面试官还会继续追问:

  1. 你能从源码角度来讲解下Spring具体的解决流程吗?
  2. 为什么要使用三级缓存呢,两级不可以吗?
  3. 为什么spring官方更推荐使用构造注入呢?

类似的问题还有很多,这些问题其实背后蕴含着Spring的一些核心技术点,包括:

  • Spring Bean的创建流程
  • Spring的三级缓存机制
  • Spring AOP的相关原理

下面我会对这些知识点进行深入的讲解,相信通过这篇文章的学习你一定能够从容的回答类似的问题了。

Spring Bean的生命周期

说到Spring Bean的创建流程,很多人可能会有这样的疑问:创建Bean不就是调用构造方法实例化对象吗?答案显然是否定的,Spring Bean是整个Spring IOC容器的核心,是有完整的生命周期的。学过Spring的小伙伴对下面这张图应该都不陌生,图中展示的就是Spring Bean生命周期的各个阶段了。

那么如何进行验证呢?很简单,我们写个Spring的测试程序即可。

Spring测试程序

首先使用开发工具创建一个普通的Maven工程,然后按照下面步骤完善整个测试程序。

  1. 引入Maven依赖
        <!-- 引入spring ioc容器功能 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.1.15.RELEASE</version>
        </dependency>
        <!-- 引入java注解 -->
        <dependency>
            <groupId>javax.annotation</groupId>
            <artifactId>javax.annotation-api</artifactId>
            <version>1.3.2</version>
        </dependency>
        <!-- 添加junit测试 -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
  1. 创建测试Bean
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.*;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

public class RuoxiyuanBean implements BeanNameAware, BeanFactoryAware, 
    ApplicationContextAware, InitializingBean, DisposableBean {

    private String status;

    public RuoxiyuanBean() {
        System.out.println("实例化Bean...");
    }
    @Override
    public void setBeanName(String name) {
        System.out.println("调用BeanNameAware的setBeanName方法...");
    }
    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        System.out.println("调用BeanFactoryAware的setBeanFactory方法...");
    }
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        System.out.println("调用ApplicationContextAware的setApplicationContext方法...");
    }
    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("调用InitializingBean的afterPropertiesSet方法...");
    }
    public void initMethod(){
        System.out.println("调用定制的初始化方法init-method...");
    }
    //实际使用较多一些
    @PostConstruct
    public void postConstruct(){
        System.out.println("调用postConstruct方法....");
    }
    @PreDestroy
    public void preDestroy(){
        System.out.println("调用preDestroy方法....");
    }
    @Override
    public void destroy() throws Exception {
        System.out.println("调用DisposableBean的destroy方法");
    }
    public void destroyMethod(){
        System.out.println("调用定制的销毁方法destroy-method...");
    }
}

继续创建一个BeanPostProcessor类

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component;

public class MyBeanPostProcessor implements BeanPostProcessor {

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        if("ruoxiyuanBean".equalsIgnoreCase(beanName)){
            System.out.println("调用BeanPostProcessor的预初始化方法...");
        }
        return bean;
    }
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if("ruoxiyuanBean".equalsIgnoreCase(beanName)){
            System.out.println("调用BeanPostProcessor的后初始化方法...");
        }
        return bean;
    }
}
  1. 在resources目录下新建配置文件applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       https://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       https://www.springframework.org/schema/context/spring-context.xsd">

    <bean id="ruoxiyuanBean" class="com.rxy.RuoxiyuanBean" init-method="initMethod" destroy-method="destroyMethod"></bean>
    <bean id="myBeanPostProcessor" class="com.rxy.MyBeanPostProcessor"></bean>
    <!-- 开启注解扫描 -->
    <context:component-scan base-package="com.rxy" />
</beans>
  1. 在test/java目录下创建测试类
import com.rxy.RuoxiyuanBean;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class SpringTest {
    @Test
    public void testIoC() {
        ClassPathXmlApplicationContext applicationContext =
                new ClassPathXmlApplicationContext("classpath:applicationContext.xml");
        RuoxiyuanBean ruoxiyuanBean = applicationContext.getBean(RuoxiyuanBean.class);
        System.out.println(ruoxiyuanBean);
        applicationContext.close();  //关闭容器
    }
}

选中测试类运行,打印情况如下图所示,和我们的预期结果是一致的。细心的小伙伴可能会发现【设置属性】这个步骤并没有体现,别着急,我们把它放到源码阶段来揭晓

值的说明的是 @PostConstruct@PreDestroy 注解并不是由Spring提供,而是从Java EE5规范开始新增的两个影响Servlet生命周期的注解。其中 @PostConstruct 修饰的方法会在服务器加载Servlet的时候运行,并且只会被服务器执行一次,实际开发中我们常使用该注解来对项目做一些初始化操作。

源码分析Spring Bean的创建流程

有了前面的铺垫我们就可以顺利的进入到源码分析阶段了,我们想知道Spring Bean的创建流程是怎样的,并且都是在什么时候哪个方法里调用这些生命周期方法的?下面我们逐步进行源码分析。

1. Spring源码构建

为了更好的阅读和研究源码,我们需要构建Spring源码工程,Spring源码构建过程并不复杂,但是比较耗费时间,可以通过搜索引擎参考对应的文章,这里仅给出主要步骤:

  • 下载源码( github搜索spring)
  • 安装gradle 5.6.3(类似于maven)
  • 将源码导入到IDEA中
  • 编译工程(顺序:core-oxm-context-beans-aspects-aop)
    如何编译:工程—>tasks—>other—>运行compileTestJava
  • 在源码工程上新建一个gradle类型的module,作为源码测试工程

测试工程的配置文件,测试类和相关的Bean同我们之前编写好的测试程序,唯一需要修改的是 build.gradle 文件,这个文件是gradle项目的构建文件(类似于pom.xml)。

sourceCompatibility = 11

repositories {
  mavenCentral()
}

dependencies {
  compile(project(":spring-context"))
  testCompile group: 'junit',name: 'junit',version: '4.12'
}
2. 源码阅读技巧

构建完源码后,我们该从何入手?相信很多人第一次接触到源码都会有种手无足措的感觉。实际上源码分析也是讲究方法和技巧的。

  • 读源码原则
    定焦原则:抓主线
    宏观原则:站在上帝视角,关注源码结构和业务流程(淡化具体某行代码的编写细节)
  • 读源码技巧
    关键位置断点(观察调用栈)
    善用反调( Find Usages)
    积累经验(比如spring框架中doXXX方法,真正做处理的地方)

通常来说,阅读大型项目的源码无外乎两种方法:

  • 自上而下(Top-Down):从最顶层或最外层的代码一步步深入。通俗地说,就是从 main 函数开始阅读,逐渐向下层层深入,直到抵达最底层代码。这个方法的好处在于,你遍历的是完整的顶层功能路径,这对于你了解各个功能的整体流程极有帮助。
  • 自下而上(Bottom-Up):跟自上而下相反,是指先独立地阅读和搞懂每个组件的代码和实现机制,然后不断向上延展,并最终把它们组装起来。该方法不是沿着功能的维度向上溯源的,相反地,它更有助于你掌握底层的基础组件代码。
3. Bean生命周期关键时机点

读源码不能着急,首先我们要明确每次读源码的目的或者说要解决什么问题。那么本次我们的目的就是了解SpringBean的具体创建流程,并由此引申的问题就是SpringBean在什么地方调用的各个生命周期方法?

明确目的后我们就可以开始打断点分析了,断点要打在关键位置,那么具体应该在哪?不难想到应该是在各个生命周期方法上,我们通过对比观察不同方法的调用栈从而找到一些线索。

举个例子,在RuoxiyuanBean的构造方法上打上断点,然后以debug模式运行测试程序,把详细的调用栈信息记录下来,如下图所示:

调用栈的具体信息:

同样的方式,我们去掉上一个断点,然后在afterPropertiesSet方法上打上新的断点,运行测试类并记录调用栈:

其他的生命周期方法也都可以按照这种方式记录下调用栈,这里就不在做演示了。通过这些调用栈图对比我们能得到两个重要的信息:

  1. 通过观察左边的调用栈,我们发现一个规律,这些生命周期方法的层级调用从 refresh() 方法一直到 doCreateBean() 方法都是一致的,因此我们可以断定这些方法的调用过程就是Spring Bean的创建子流程
  2. 这些生命周期方法的调用栈从 doCreateBean() 方法后就分道扬镳了,各自调用了不同的方法完成相关的逻辑。因此我们可以确定 doCreateBean() 方法就是完成生命周期方法调用的关键步骤。

下面我们针对这些信息做进一步的说明。

4. Spring IoC容器初始化主流程

根据上面的调试分析,我们发现 Bean对象创建的几个关键时机点代码层级的调用都在AbstractApplicationContext 类 的 refresh 方法中,可见这个方法对于Spring IoC 容器初始化来说相当关键。

在Spring IoC容器中还有一个重要的接口:BeanFactoryPostProcessor(bean工厂的后置处理容器)。我们同样可以创建该接口的实现类并在实现方法上打断点观察调用栈,最终得到的汇总信息如下:

毫无意外这个接口对应实现类的实例化及方法调用也都是在refresh方法中,那么我们就通过查看 refresh 方法的源码来俯瞰容器创建的主体流程。核心源码具体如下:

@Override
public void refresh() throws BeansException, IllegalStateException {
    //对象锁加锁
    synchronized (this.startupShutdownMonitor) {
        prepareRefresh();
        ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
        prepareBeanFactory(beanFactory);
        try {
            postProcessBeanFactory(beanFactory);
            invokeBeanFactoryPostProcessors(beanFactory);
            registerBeanPostProcessors(beanFactory);
            initMessageSource();
            initApplicationEventMulticaster();
            onRefresh();
            registerListeners();
            finishBeanFactoryInitialization(beanFactory);
            finishRefresh();
        } catch (BeansException ex) {
            ...
        }
    }
}

从源码中可以看出refresh方法中一共调用了12个子方法来完成整个IOC容器的创建逻辑。下面用一张图来说明这些方法的具体作用:

5. Spring IoC的容器体系

不知道大家是否注意到refresh方法中的带参数的子方法传递的参数都是beanFactory对象,实际的类是DefaultListableBeanFactory,而该类实现了BeanFactory接口,讲到这我们就扩展一下关于Spring IoC的容器体系。

IoC容器是Spring的核心模块,是抽象了对象管理、依赖关系管理的框架解决方案。Spring 提供了很多的容器,其中 BeanFactory 是顶层容器(根容器),不能被实例化,它定义了所有 IoC 容器必须遵从的一套原则,具体的容器实现可以增加额外的功能,比如我们常用到的ApplicationContext,其下更具体的实现如 ClassPathXmlApplicationContext 包含了解析 xml 等一系列的内容,AnnotationConfigApplicationContext 则是包含了注解解析等一系列的内容。Spring IoC 容器继承体系非常优雅,需要使用哪个层次用哪个层次即可,不必使用功能大而全的。

BeanFactory 顶级接口方法栈如下:

BeanFactory 容器继承体系:

通过其接口设计,我们可以看到我们一贯使用的 ApplicationContext 除了继承BeanFactory的子接口,还继承了ResourceLoader、MessageSource等接口,因此其提供的功能也就更丰富了。

6. Spring Bean的创建子流程

通过最开始的关键时机点分析,我们知道Bean创建子流程入口在AbstractApplicationContext#refresh()方法的finishBeanFactoryInitialization(beanFactory) 处。接下来我们通过调试模式走一遍这个创建子流程。

进入finishBeanFactoryInitialization方法,关键逻辑在最后一行

继续进入DefaultListableBeanFactory类的preInstantiateSingletons方法,我们找到下面部分的代码,看到工厂Bean或者普通Bean,最终都是通过getBean的方法获取实例

下一步进入AbstractBeanFactory#getBean(每个Bean创建的真正入口)方法,该方法又调用了doGetBean方法。

继续跟踪下去,我们进入到了AbstractBeanFactory类的doGetBean方法,这个方法中的代码很多,我们直接找到核心部分,通过getSingleton方法获取bean实例(注意参数二是一个lamda表达式)

接着进入到DefaultSingletonBeanRegistry类的getSingleton方法,这里又回调了参数二(lmbda表达式)的getObject方法(也就是上一步的createBean()方法)。

接着进入到AbstractAutowireCapableBeanFactory#createBean(真真正正创建Bean)方法,找到以下代码部分:

最终调用doCreateBean方法获取到了bean实例并一步步往上返回该实例对象。

由于这里Bean创建的子流程不是重点内容,因此这里并没有将完整的源码贴出。我们通过一张时序图来描述整个调用过程。

前面我们通过测试程序了解了Spring Bean的生命周期,这次我们通过源码方式来进一步验证生命周期涉及的各个过程。关键方法就是:AbstractAutowireCapableBeanFactory#doCreateBean(),也就是Bean创建子流程的最后一步。
核心源码如下:

/**
 * Spring Bean生命周期整体执行顺序为:
 * Bean实例化 --> 设置属性 --> BeanNameAware --> BeanFactoryAware 
 * --> ApplicationContextAware --> BeanPostProcessor#before --> postConstruct
 * --> afterPropertiesSet --> init-method --> BeanPostProcessor#after
 * 所有步骤由该方法完成
 */
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)
    throws BeanCreationException {
    // 开始实例化bean
    BeanWrapper instanceWrapper = null;
    if (mbd.isSingleton()) {
        instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);
    }
    if (instanceWrapper == null) {
        // 第一步:创建bean实例
        instanceWrapper = createBeanInstance(beanName, mbd, args);
    }
    final Object bean = instanceWrapper.getWrappedInstance();
    Class<?> beanType = instanceWrapper.getWrappedClass();
    if (beanType != NullBean.class) {
        mbd.resolvedTargetType = beanType;
    }
    //提前暴露对象,加入三级缓存
    boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
                                      isSingletonCurrentlyInCreation(beanName));
    if (earlySingletonExposure) {
        addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
    }
    // 开始初始化bean实例
    Object exposedObject = bean;
    try {
        //第二步:设置属性值
        populateBean(beanName, mbd, instanceWrapper);
        // 调用初始化方法,应用BeanPostProcessor后置处理器
        exposedObject = initializeBean(beanName, exposedObject, mbd);
    }
    ...
    return exposedObject;
}
/**
 * 初始化Bean
 * 包括Bean后置处理器初始化
 * Bean的一些初始化方法的执行init-method
 * Bean的实现的声明周期相关接口的属性注入
 */
protected Object initializeBean(final String beanName, final Object bean, @Nullable RootBeanDefinition mbd) {
    if (System.getSecurityManager() != null) {
        AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
            invokeAwareMethods(beanName, bean);
            return null;
        }, getAccessControlContext());
    }else {
        // 第 3 步:调用BeanNameAware的setBeanName方法
        // 第 4 步:调用BeanFactoryAware的setBeanFactory方法
        invokeAwareMethods(beanName, bean);
    }
    Object wrappedBean = bean;
    if (mbd == null || !mbd.isSynthetic()) {
        // 第 5 步:调用ApplicationContextAware的setApplicationContext方法
        // 第 6 步:调用BeanPostProcessor的预初始化方法
        // 先调用的是ApplicationContextAwareProcessor#invokeAwareInterfaces(也是一个BeanPostProcessor)
        wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
    }
    try {
        // 第 7 步:调用InitializingBean的afterPropertiesSet方法: InitializingBean#afterPropertiesSet
        // 第 8 步:调用定制的初始化方法init-method:invokeCustomInitMethod
        invokeInitMethods(beanName, wrappedBean, mbd);
    }
    if (mbd == null || !mbd.isSynthetic()) {
        // 第 9 步:调用BeanPostProcessor的后初始化方法
        wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
    }
    return wrappedBean;
}

Spring Bean生命周期源码图:

同样,对于各个生命周期方法的执行细节不是本讲的重点,这里不再展开。至此我们对Spring Bean的创建流程就分析完毕了。
有了前面的铺垫后,我们就可以顺序的进入到今天探讨的主题——Spring循环依赖的解决方案。

Spring IOC循环依赖问题
1. 什么是循环依赖?

循环依赖其实就是循环引用,也就是两个或者两个以上的 Bean 互相持有对方,最终形成闭环。比如A依赖于B, B依赖于C, C又依赖于A。

注意,这里不是函数的循环调用,是对象的相互依赖关系。循环调用其实就是一个死循环,除非有终结条件。

2. Spring循环依赖场景

我们知道依赖注入方法主要有两种:构造器注入、Field 属性注入(setter注入)。而Spring Bean又分为两种类型:(singleton)单例 bean和(prototype)原型 bean。因此Spring中循环依赖场景有如下四种:

  • 单例 bean 构造器参数循环依赖(无法解决)
  • 单例bean通过setXxx或者@Autowired进行循环依赖
  • 原型 bean构造器参数循环依赖(无法解决)
  • 原型 bean通过setXxx或者@Autowired进行循环依赖(无法解决)

首先讲一下原型bean,对于原型bean的初始化过程中不论是通过构造器参数循环依赖还是通过setXxx方法产生循环依赖, Spring都会直接报错处理。

因为原型模式每次都是重新生成一个全新的bean,根本没有缓存一说。这将导致实例化A完,填充发现需要B,实例化B完又发现需要A,而每次的A又都要不一样,所以死循环的依赖下去。唯一的做法就是利用循环依赖检测,发现原型模式下存在循环依赖并抛出异常。

来看一下Spring对原型Bean的循环依赖处理:核心逻辑在AbstractBeanFactory类的doGetBean方法

// 创建prototype类型对象前的检查
// 如果是prototype类型并且正在创建中(说明产生了循环依赖),则直接抛出异常(无法处理)
if (isPrototypeCurrentlyInCreation(beanName)) {
    throw new BeanCurrentlyInCreationException(beanName);
}
protected boolean isPrototypeCurrentlyInCreation(String beanName) {
    Object curVal = this.prototypesCurrentlyInCreation.get();
    return (curVal != null &&
            (curVal.equals(beanName) || (curVal instanceof Set && ((Set<?>) curVal).contains(beanName))));
}
//如果检查通过则创建prototype类型对象
if (mbd.isPrototype()) {
    // It's a prototype -> create a new instance.
    Object prototypeInstance = null;
    try {
        // 创建原型bean之前标记bean正在被创建
        beforePrototypeCreation(beanName);
        // 创建原型实例
        prototypeInstance = createBean(beanName, mbd, args);
    }
    finally {
        // 创建原型bean之后移除正在被创建的标记
        afterPrototypeCreation(beanName);
    }
}

通过源码可知在获取bean之前如果这个原型bean正在被创建则直接抛出异常。原型bean在创建之前会进行标记这个beanName正在被创建,等创建结束之后会删除标记。该标记定义AbstractBeanFactory类中。

// Names of beans that are currently in creation
private final ThreadLocal<Object> prototypesCurrentlyInCreation =
            new NamedThreadLocal<>("Prototype beans currently in creation");

结论:显然Spring 不支持原型 bean 的循环依赖。
那么单例 bean 构造器参数循环依赖为什么也不支持?下面我们会先讲一下setter方式循环依赖的处理方法,然后再来回答这个问题。

3. 循环依赖处理机制分析

通常我们分析底层原理时都是基于最简单的案例进行,这样有助于我们快速分析出核心的逻辑。因此我们在这里创建两个简单的类A和B,并让它们产生循环依赖关系。

@Component
public class A {
    @Autowired
    private B b;
    public void setB(B b) {
        this.b = b;
    }
}
@Component
public class B {
    @Autowired
    private A a;
    public void setA(A a) {
        this.a = a;
    }
}

有了前面Bean创建过程的源码分析基础,这里源码分析就会跳过一些不必要的过程,直接抓住重点内容进行解读。
循环依赖源码调用过程:(A <—> B)
1.开始创建Bean A(入口AbstractBeanFactory#getBean)
该方法又调用了doGetBean方法,首先会尝试从缓存中获取Bean实例

2.进入到DefaultSingletonBeanRegistry#getSingleton单参数方法,这里会依次从一级、二级、三级缓存中去获取,因为当前bean还未开始创建,所以不能获取到实例。

3.实例化Bean A(AbstractAutowireCapableBeanFactory#doCreateBean)

4.实例化完Bean A后,会提前曝光对象,加入三级缓存

加入三级缓存调用的是DefaultSingletonBeanRegistry#addSingletonFactory方法

5.填充属性b(AbstractAutowireCapableBeanFactory#populateBean)

进入applyPropertyValues方法,这里需要对属性值进行解析

继续进入BeanDefinitionValueResolver的resolveValueIfNecessary方法

继续进入resolveReference方法

你会发现在这里实际调用了AbstractBeanFactory#getBean(也就是Bean创建流程的入口)
6.开始创建Bean B(6-10步过程同上)
7.尝试从各级缓存获取B实例,也未获取到
8.实例化Bean B
9.提前曝光对象,将Bean B的对象工厂加入三级缓存
10.填充属性a,显然又来到了AbstractBeanFactory#getBean方法,此时getBean获取的是a的实例。
11.尝试从各级缓存获取B实例,此时由于Bean A提前暴露在三级缓存,因此通过getSingleton方法获取到了该实例的对象工厂,并且调用工厂的getObject()方法获取到了Bean A的引用。(参照第2步代码示例)
12.回到第10步,此时applyPropertyValues方法获得了Bean A,然后对属性a进行赋值操作。

到这里就完成了Bean B生命周期的第二步设置属性值。
13.BeanB继续完成后续的初始化操作,也就是createBean方法执行完毕。
14.如果你对前面讲的Bean创建子流程的过程还有印象的话,应该知道下一步是回到DefaultSingletonBeanRegistry#getSingleton方法。来看关键代码

查看addSingleton方法,该方法会将BeanB加入到一级缓存中。

到这里整个Bean B的创建过程才算真正完成了。
15.回到第5步(填充属性b),此时Bean A就通过bean工厂获得了Bean B实例并完成了属性的设置。
16.Bean A继续完成后续的一系列初始化操作
17.最后Bean A也加入到一级缓存中,至此整个过程结束。

整个调试过程还是相对有些复杂的,需要有一定耐心,在调试过程中关注重点,多思考,多记录。

结论:循环依赖问题采用提前暴露策略解决,提前暴露刚完成构造器注入但是还没有完成其他步骤的bean的对象工厂。

循环源码调用过程的时序图:

【补充说明】关于构造方法的循环依赖

首先对于单例bean而言,和原型bean类似也有一个创造中的标识,在bean开始创建前会先标记为创建中,在创建完成之后会移除标识。相关的源码如下:

// DefaultSingletonBeanRegistry类
public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
    synchronized (this.singletonObjects) {
        Object singletonObject = this.singletonObjects.get(beanName);
        if (singletonObject == null) {
            // 创建之前先标识该bean正在被创建,因为springbean创建过程复杂步骤很多,需要标识
            // this.singletonsCurrentlyInCreation.add(beanName)
            beforeSingletonCreation(beanName);
            boolean newSingleton = false;
            try {
                // 传过来的调用,lamda表达式使用,从ObjectFactory中获取bean
                singletonObject = singletonFactory.getObject();
                newSingleton = true;
            } finally {
                // 创建完成之后移除标识
                // this.singletonsCurrentlyInCreation.remove(beanName)
                afterSingletonCreation(beanName);
            }
            if (newSingleton) {
                //缓存bean对象
                addSingleton(beanName, singletonObject);
            }
        }
        return singletonObject;
    }
}

标识singletonsCurrentlyInCreation定义在DefaultSingletonBeanRegistry类中,存储了当前正在创建中的所有单例bean名称。

/** Names of beans that are currently in creation. */
private final Set<String> singletonsCurrentlyInCreation = Collections.newSetFromMap(new ConcurrentHashMap<>(16));

其实根据Spring的循环依赖处理机制你应该能明白为什么不支持构造方法的循环依赖。那么我们先通过源码来验证这一点并快速找到spring的处理方式。

这里如果你不想一步步调试源码来看效果,那么就可以通过调用栈的形式快速过一遍流程。首先你需要把A和B类改为构造方法注入的形式。然后直接启动测试线,观察报错信息。

通过错误堆栈信息我们定位到了报错的根位置:DefaultSingletonBeanRegistry#beforeSingletonCreation,这个方法是bean创建之前的判断,判断通过则标记bean为创建中。也就是说构造方式的循环依赖在这里判断未通过因此抛出了异常。
因此我们可以在该方法上打上断点,重新运行测试类。

进入到断点处后,你可以观察左边的调用栈,从下至上依次是bean A到Bean B使用构造依赖注入创建的流程,整个过程大概就是这样:

  1. bean A开始创建,标记为创建中,调用构造方法去创建实例,但是发现需要参数b,因此需要先获取Bean B
  2. bean B开始创建,标记为创建中,调用构造方法去创建实例,发现需要参数a,因此需要先获取Bean A
  3. 又通过getBean方法获取Bean A,在创建Bean A前判断a此时是在创建中的,因此直接抛出异常。(此时实际A和B都未完成初步的实例化,也就不可能涉及到缓存操作了)
4. 循环依赖处理机制总结

Spring 的循环依赖的理论依据基于 Java 的引用传递,当获得对象的引用时,对象的属性是可以延后设置的,但是构造器必须是在获取引用之前。(因此构造注入循环依赖无法解决)

Spring通过setXxx或者@Autowired方法解决循环依赖其实是通过提前暴露一个ObjectFactory对象来完成的,简单来说ClassA在调用构造器完成对象初始化之后,在调用ClassA的setClassB方法之前就把ClassA实例化的对象通过ObjectFactory提前暴露到Spring容器中,供循环依赖的对象引用。

整体步骤:

  • Spring容器初始化ClassA通过构造器初始化对象后提前暴露到Spring容器
  • ClassA调用setClassB方法, Spring首先尝试从容器中获取ClassB,此时ClassB不存在Spring容器中
  • Spring容器初始化ClassB,同时也会将ClassB提前暴露到Spring容器中
  • ClassB调用setClassA方法, Spring从容器中获取ClassA ,因为第一步中已经提前暴露了ClassA,因此可以获取到ClassA实例
  • ClassB创建完成之后ClassA通过spring容器获取到ClassB,完成了对象初始化操作
  • 这样ClassA和ClassB都完成了对象初始化操作,解决了循环依赖问题

我们在用一张图来具体描述这个过程:

4.三级缓存机制

在源码层面Spring通过三级缓存机制实现上面的思路,巧妙的解决了循环依赖问题。在这里我们对三级缓存做一个汇总。

所谓的三级缓存指的是:

  • singletonObjects:一级缓存,里面放置的是已经完成所有创建动作的单例对象,也就是说这里存放的bean已经完成了所有创建的生命周期过程,在项目运行阶段当执行多次getBean()时就是从一级缓存中获取的。
  • earlySingletonObjects:二级缓存,里面存放的是提前曝光的已经实例化好的单例对象。与一级缓存的区别在于,该缓存所获取到的bean是还没创建完成的,比如属性填充跟初始化动作肯定还没有做完,因此仅作为指针提前曝光,方便被其他bean所引用
  • singletonFactories:三级缓存,里面存放的是要被实例化的对象的对象工厂(ObjectFactory实例对象),在需要引用提前曝光对象时再通过objectFactory.getObject()方法获取真正的实例对象。

它们都定义在DefaultSingletonBeanRegistry类中:

/** 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 HashMap<>(16);

三级缓存的添加时机:Bean对象完成实例化后,设置属性前

//调用处:AbstractAutowireCapableBeanFactory#doCreateBean
// 如果允许提前曝光,则将该bean转换成ObjectFactory并加入到三级缓存
if (earlySingletonExposure) {
    addSingletonFactory(beanName, () -> 
            getEarlyBeanReference(beanName, mbd, bean));
}
// DefaultSingletonBeanRegistry#addSingletonFactory方法
protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
    Assert.notNull(singletonFactory, "Singleton factory must not be null");
    synchronized (this.singletonObjects) {
        if (!this.singletonObjects.containsKey(beanName)) {
            // 加入三级缓存
            this.singletonFactories.put(beanName, singletonFactory);
            // 从二级缓存中移除
            this.earlySingletonObjects.remove(beanName);
            // 加入到注册单例bean(有序的set集合)
            this.registeredSingletons.add(beanName);
        }
    }
}

一级缓存的添加时机:在bean创建完成后

// 调用处:DefaultSingletonBeanRegistry#getSingleton(String, ObjectFactory)
if (newSingleton) {
    addSingleton(beanName, singletonObject);
}
// addSingleton(beanName, Object)方法
protected void addSingleton(String beanName, Object singletonObject) {
    synchronized (this.singletonObjects) {
        // 添加到一级缓存
        this.singletonObjects.put(beanName, singletonObject);
        // 从三级缓存中移除
        this.singletonFactories.remove(beanName);
        // 从二级缓存中移除
        this.earlySingletonObjects.remove(beanName);
        // 添加到已注册bean(有序set集合)
        this.registeredSingletons.add(beanName);
    }
}

二级缓存的添加时机
前面我们讲解循环依赖的时候似乎并没有提及二级缓存(如果仔细的同学应该能发现是有的),那么这时候如何知道二级缓存的添加时机呢,在教大家一个方法—反调法。

  1. 在 DefaultSingletonBeanRegistry 类中搜索 earlySingletonObjects.put,你能够找到一个 getSingleton 方法
  2. 在IDEA中选中方法右键选择【Find Usages】,你能够找到在同类中一个单参数的重载方法 getSingleton 调用了它
  3. 最后选中这个重载方法,同样通过【Find Usages】查看调用处,你能发现一个关键的方法 doGetBean,就是获取bean的入口方法了

二级缓存的添加时机:从三级缓存中获取到对象工厂,并且调用对象工厂的getObject方法获取到bean实例之后

// 调用处:AbstractBeanFactory#doGetBean
// 尝试从各级缓存中获取bean
Object sharedInstance = getSingleton(beanName);
// DefaultSingletonBeanRegistry#getSingleton(String, boolean)方法
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    // 从一级缓存中获取
    Object singletonObject = this.singletonObjects.get(beanName);
    // 判断当前bean是否正在创建中
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        synchronized (this.singletonObjects) {
            // 从二级缓存中获取
            singletonObject = this.earlySingletonObjects.get(beanName);
            // 判断是否允许循环依赖
            if (singletonObject == null && allowEarlyReference) {
                // 从三级缓存中获取
                ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                if (singletonFactory != null) {
                    // 获取到了对象工厂之后,调用getObject方法获取真正的实例
                    singletonObject = singletonFactory.getObject();
                    // 将获取到的实例放入二级缓存中
                    this.earlySingletonObjects.put(beanName, singletonObject);
                    // 从三级缓存中移除
                    this.singletonFactories.remove(beanName);
                }
            }
        }
    }
    return singletonObject;
}

有关三级缓存机制我们就总结到这,接下来我们再来回答一个关键的问题:为什么要使用三级缓存呢,两级不可以吗?
其实这个问题也可以转换为别的形式,比如三级缓存为什么要缓存一个ObjectFactory对象,为什么不直接缓存实例?或者在循环依赖处理过程中二级缓存似乎没起到什么作用,那么它的意义何在,是否可以去掉?
要回答这个问题,需要你对Spring AOP的相关原理有所了解。

Spring AOP的原理

关于Spring AOP的源码这里并不会太过于深入,之后我会专门写一篇关于AOP源码的详细分析的文章。

1. 时机点分析

首先需要准备AOP的基础案例(省略),然后在测试类的getBean处打上断点,运行测试类,观察单例池中ruoxiyuanBean对象。

我们发现在getBean之前,RuoxiyuanBean对象已经产生,而且该对象是一个代理对象(Cglib代理对象),我们断定,容器初始化过程中目标Ban已经完成了代理,返回了代理对象。

2.代理对象创建流程

通过前面对Bean的创建流程分析,我们可以大胆猜想一下代理对象产生的位置,应该是在原始Bean对象实例化和填充属性后做一些后置处理时创建的。即AbstractAutowireCapableBeanFactory#doCreateBean方法中如下位置,打上断点F5进入该方法。

在该方法的最后对包装Bean进行了赋值,我们进入该方法

在该方法中循环所有的BeanPostProcessor进行一一处理,我们找到与代理相关的类

F5进入方法,来到创建代理对象的后置处理器AbstractAutoProxyCreator#postProcessAfterInitialization

下一步,进入同类下的wrapIfNecessary方法,在这里开始创建代理对象。

我们分析到这就结可以结束了,后续就是创建代理对象的具体过程了,分析这个流程我们主要想阐述两个观点:

  1. 按照正常情况下(没有产生循环依赖)bean的代理对象是从填充属性后做一些后置处理时开始创建的
  2. wrapIfNecessary方法会返回一个创建好的代理对象
3.循环依赖Bean代理对象创建时机

我们先直接给出结论,然后再通过源码来验证这一点。对于A 与 B类产生循环依赖的情况,A创建代理对象的时机应该是在AbstractAutowireCapableBeanFactory#getEarlyBeanReference方法。

分析:前面的过程是 A实例化 —> B实例化 —> B填充属性,此时会调用DefaultSingletonBeanRegistry#getSingleton方法从三级缓存中获取到A的引用,但是这里并不是直接获取,而是取出三级缓存中存放的ObjectFactory对象,然后从该工厂对象中通过getObject获取到对象引用。

回顾A在实例化后存入三级缓存的ObjectFactory是什么?来到代码AbstractAutowireCapableBeanFactory#doCreateBean

这里显示存放的是一个lamda表达式,当我们调用getObject方法时实际调用的是AbstractAutowireCapableBeanFactory#getEarlyBeanReferencegetEarlyBeanReference方法。

进入该方法,打上断点,这个方法的逻辑就是遍历bean所有的后置处理器,如果有指定的则调用指定的方法,否则直接返回bean实例。观察我们获取到的SmartInstantiationAwareBeanPostProcessor接口实现,是一个AspectJAwareAdvisorAutoProxyCreator对象,该对象继承自AbstractAutoProxyCreator类。

我们进入AbstractAutoProxyCreator#getEarlyBeanReference方法,该方法将Bean A加入到提前曝光对象容器中,然后调用了wrapIfNecessary方法,该方法由上面流程可知会产生一个Bean A的代理对象并返回。而Bean B的代理对象还是正常流程产生。

当Bea B创建完成后,Bean A执行到AbstractAutoProxyCreator#postProcessAfterInitialization方法时,这里的缓存判断为false表示Bean A已经创建了代理对象,就直接返回了。

至此关于AOP源码的分析就全部结束了,那么我们来总结一下:

  1. Spring会针对需要创建代理对象的bean添加一个后置处理器,即SmartInstantiationAwareBeanPostProcessor接口的实现,那么具体的实现类就是AbstractAutoProxyCreator。通过该类的getEarlyBeanReference方法获取到代理对象
  2. Spring使用二级缓存来存储通过提前曝光的对象工厂获取到的实例对象,那么该对象可能是被包装后的代理对象,也可能是原始bean实例

不知道通过AOP源码的分析后大家是否可以理解为什么不使用两级缓存而要使用三级缓存呢?我这里再来解释一下。

从Spring的角度来讲,使用三级缓存先缓存一个对象工厂,如果当前bean不存在循环依赖问题,这个三级缓存就没有什么实质的作用了。如果存在循环依赖并且该bean不需要被代理呢,那么二级缓存就没有什么实质的作用了,因为此时从对象工厂获得的就是bean实例化的对象。但是如果存在循环依赖并且该bean需要被代理时,就需要通过对象工厂对提前曝光的对象进行代理包装处理了,并且需要一个缓存来存储这个代理对象,以便后续其他的对象直接引用。

所以你可以说Spring之所以使用三级缓存的机制(而不是两级)主要是为了解决产生循环依赖的bean同时也需要被代理的需求,或者说为了保证提前曝光的bean在被提前引用之前可以被Spring AOP进行代理。

而从框架的角度来讲,多加一层缓存和对象工厂接口,可以保证整个架构的可扩展性,事实上即使没有AOP的需求,你也可以通过重写SmartInstantiationAwareBeanPostProcessor后置处理器对提前曝光的实例,在被提前引用时进行一些特殊的操作。这种设计思想也是非常值得我们学习和借鉴的。

扩展内容
1.spring为什么更推荐使用构造注入?

你可能会说出以下的一些原因:

  • 保证依赖不可变(final关键字)
  • 保证依赖不为空(省去了我们对其检查)
  • 保证返回客户端(调用)的代码的时候是完全初始化的状态

那么站在循环依赖的角度回答就是构造器注入可以有效避免循环依赖,因为在启动时如果存在构造器的循环依赖就会直接抛出异常。

从这个角度也说明了一个问题,就是项目中如果存在大量的循环依赖的话,显然并不是一个很好的现象,应该在代码设计时尽可能避免出现循环依赖的情况。

2. 如何检测项目中存在的循环依赖类

业界存在一款优秀的开源工具,它专门用于量化代码的各种度量指标,其中就包含了代码循环依赖分析,这款工具就是JDepend。你可以在GitHub上找到它的源码和使用方法。它可以对指定的包结构进行分析,给出系统中存在循环依赖代码的提示(存在循环依赖关系的包、所依赖的包和被依赖的包)。
关于这款工具的使用方法这里就不做介绍了,相信作为优秀工程师的你有能力去自学搞定它。

3. 如何解决代码中存在的循环依赖问题呢?

这个作为最后的思考题留给大家

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266