我们一起聊聊什么是Spring的循环依赖

开发 架构
当我们使用构造方法进行注入时,就会遇到这种情况。如果您使用其它类型的注入,你应该不会遇到这个问题。因为它是在需要时才会被注入,而不是上下文加载被要求注入。

1. 什么是循环依赖?

一般场景是一个Bean A依赖Bean B,而Bean B也依赖Bean A :Bean A → Bean B → Bean A当然我们也可以添加更多的依赖层次,比如:Bean A → Bean B → Bean C → Bean D → Bean E → Bean A

2. Spring中的循环依赖

当Spring上下文在加载所有的bean时,他会尝试按照他们他们关联关系的顺序进行创建。比如,如果不存在循环依赖时,例如:Bean A → Bean B → Bean CSpring会先创建Bean C,再创建Bean B(并将Bean C注入到Bean B中),最后再创建Bean A(并将Bean B注入到Bean A中)。但是,如果我们存在循环依赖,Spring上下文不知道应该先创建哪个Bean,因为它们依赖于彼此。在这种情况下,Spring会在加载上下文时,抛出一个BeanCurrentlyInCreationException。当我们使用构造方法进行注入时,就会遇到这种情况。如果您使用其它类型的注入,你应该不会遇到这个问题。因为它是在需要时才会被注入,而不是上下文加载被要求注入。

3. 让我们看一个例子

我们定义两个Bean并且互相依赖(通过构造函数注入)。

package com.test.demo.service;

import org.springframework.stereotype.Component;

import javax.annotation.Resource;

@Component
public class A {

    private B b;

    public A (B b) {
        this.b = b;
    }
}

package com.test.demo.service;

import org.springframework.stereotype.Component;

import javax.annotation.Resource;

@Component
public class B {

    private A a ;

    public B(A a) {
        this.a = a;
    }
}

这个时候我们我们启动项目就会发现项目启动不起来,如下提示信息

/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/bin/java -XX:TieredStopAtLevel=1 -noverify -Dspring.output.ansi.enabled=always -Dcom.sun.management.jmxremote -Dspring.jmx.enabled=true -Dspring.liveBeansView.mbeanDomain -Dspring.application.admin.enabled=true -javaagent:/private/var/folders/03/_9q6fw895913yznz6wq544_00000gn/T/AppTranslocation/332DF0A0-AF1B-49F1-8739-1BF4A9F4BAF7/d/IntelliJ IDEA.app/Contents/lib/idea_rt.jar=50456:/private/var/folders/03/_9q6fw895913yznz6wq544_00000gn/T/AppTranslocation/332DF0A0-AF1B-49F1-8739-1BF4A9F4BAF7/d/IntelliJ IDEA.app/Contents/bin -Dfile.encoding=UTF-8 -classpath /Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/jre/lib/charsets.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/jre/lib/deploy.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/jre/lib/ext/cldrdata.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/jre/lib/ext/dnsns.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/jre/lib/ext/jaccess.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/jre/lib/ext/jfxrt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/jre/lib/ext/localedata.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/jre/lib/ext/nashorn.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/jre/lib/ext/sunec.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/jre/lib/ext/sunjce_provider.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/jre/lib/ext/sunpkcs11.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/jre/lib/ext/zipfs.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/jre/lib/javaws.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/jre/lib/jce.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/jre/lib/jfr.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/jre/lib/jfxswt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/jre/lib/jsse.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/jre/lib/management-agent.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/jre/lib/plugin.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/jre/lib/resources.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/jre/lib/rt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/lib/ant-javafx.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/lib/dt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/lib/javafx-mx.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/lib/jconsole.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/lib/packager.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/lib/sa-jdi.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/lib/tools.jar:/Users/haolonglong/IdeaProjects/project_boot/target/classes:/Users/haolonglong/.m2/repository/org/springframework/boot/spring-boot-starter-web/2.4.0-SNAPSHOT/spring-boot-starter-web-2.4.0-20200709.121446-178.jar:/Users/haolonglong/.m2/repository/org/springframework/boot/spring-boot-starter-json/2.4.0-SNAPSHOT/spring-boot-starter-json-2.4.0-20200709.121446-178.jar:/Users/haolonglong/.m2/repository/com/fasterxml/jackson/core/jackson-databind/2.11.1/jackson-databind-2.11.1.jar:/Users/haolonglong/.m2/repository/com/fasterxml/jackson/core/jackson-annotations/2.11.1/jackson-annotations-2.11.1.jar:/Users/haolonglong/.m2/repository/com/fasterxml/jackson/core/jackson-core/2.11.1/jackson-core-2.11.1.jar:/Users/haolonglong/.m2/repository/com/fasterxml/jackson/datatype/jackson-datatype-jdk8/2.11.1/jackson-datatype-jdk8-2.11.1.jar:/Users/haolonglong/.m2/repository/com/fasterxml/jackson/datatype/jackson-datatype-jsr310/2.11.1/jackson-datatype-jsr310-2.11.1.jar:/Users/haolonglong/.m2/repository/com/fasterxml/jackson/module/jackson-module-parameter-names/2.11.1/jackson-module-parameter-names-2.11.1.jar:/Users/haolonglong/.m2/repository/org/springframework/boot/spring-boot-starter-tomcat/2.4.0-SNAPSHOT/spring-boot-starter-tomcat-2.4.0-20200709.121446-178.jar:/Users/haolonglong/.m2/repository/org/apache/tomcat/embed/tomcat-embed-core/9.0.36/tomcat-embed-core-9.0.36.jar:/Users/haolonglong/.m2/repository/org/glassfish/jakarta.el/3.0.3/jakarta.el-3.0.3.jar:/Users/haolonglong/.m2/repository/org/apache/tomcat/embed/tomcat-embed-websocket/9.0.36/tomcat-embed-websocket-9.0.36.jar:/Users/haolonglong/.m2/repository/org/springframework/spring-web/5.3.0-M1/spring-web-5.3.0-M1.jar:/Users/haolonglong/.m2/repository/org/springframework/spring-beans/5.3.0-M1/spring-beans-5.3.0-M1.jar:/Users/haolonglong/.m2/repository/org/springframework/spring-webmvc/5.3.0-M1/spring-webmvc-5.3.0-M1.jar:/Users/haolonglong/.m2/repository/org/springframework/spring-aop/5.3.0-M1/spring-aop-5.3.0-M1.jar:/Users/haolonglong/.m2/repository/org/springframework/spring-context/5.3.0-M1/spring-context-5.3.0-M1.jar:/Users/haolonglong/.m2/repository/org/springframework/spring-expression/5.3.0-M1/spring-expression-5.3.0-M1.jar:/Users/haolonglong/.m2/repository/org/springframework/boot/spring-boot-starter/2.4.0-SNAPSHOT/spring-boot-starter-2.4.0-20200709.121446-178.jar:/Users/haolonglong/.m2/repository/org/springframework/boot/spring-boot/2.4.0-SNAPSHOT/spring-boot-2.4.0-20200709.121446-178.jar:/Users/haolonglong/.m2/repository/org/springframework/boot/spring-boot-autoconfigure/2.4.0-SNAPSHOT/spring-boot-autoconfigure-2.4.0-20200709.121446-178.jar:/Users/haolonglong/.m2/repository/org/springframework/boot/spring-boot-starter-logging/2.4.0-SNAPSHOT/spring-boot-starter-logging-2.4.0-20200709.121446-178.jar:/Users/haolonglong/.m2/repository/ch/qos/logback/logback-classic/1.2.3/logback-classic-1.2.3.jar:/Users/haolonglong/.m2/repository/ch/qos/logback/logback-core/1.2.3/logback-core-1.2.3.jar:/Users/haolonglong/.m2/repository/org/apache/logging/log4j/log4j-to-slf4j/2.13.3/log4j-to-slf4j-2.13.3.jar:/Users/haolonglong/.m2/repository/org/apache/logging/log4j/log4j-api/2.13.3/log4j-api-2.13.3.jar:/Users/haolonglong/.m2/repository/org/slf4j/jul-to-slf4j/1.7.30/jul-to-slf4j-1.7.30.jar:/Users/haolonglong/.m2/repository/jakarta/annotation/jakarta.annotation-api/1.3.5/jakarta.annotation-api-1.3.5.jar:/Users/haolonglong/.m2/repository/org/springframework/spring-core/5.3.0-M1/spring-core-5.3.0-M1.jar:/Users/haolonglong/.m2/repository/org/springframework/spring-jcl/5.3.0-M1/spring-jcl-5.3.0-M1.jar:/Users/haolonglong/.m2/repository/org/yaml/snakeyaml/1.26/snakeyaml-1.26.jar:/Users/haolonglong/.m2/repository/org/slf4j/slf4j-api/1.7.30/slf4j-api-1.7.30.jar com.test.demo.DemoApplication

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::       (v2.4.0-SNAPSHOT)

2020-07-10 08:01:20.125  INFO 879 --- [           main] com.test.demo.DemoApplication            : Starting DemoApplication using Java 1.8.0_211 on localhost with PID 879 (/Users/haolonglong/IdeaProjects/project_boot/target/classes started by haolonglong in /Users/haolonglong/IdeaProjects/project_boot)
2020-07-10 08:01:20.128  INFO 879 --- [           main] com.test.demo.DemoApplication            : No active profile set, falling back to default profiles: default
2020-07-10 08:01:21.143  INFO 879 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2020-07-10 08:01:21.155  INFO 879 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2020-07-10 08:01:21.155  INFO 879 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.36]
2020-07-10 08:01:21.226  INFO 879 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2020-07-10 08:01:21.226  INFO 879 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1051 ms
2020-07-10 08:01:21.263  WARN 879 --- [           main] ConfigServletWebServerApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'a' defined in file [/Users/haolonglong/IdeaProjects/project_boot/target/classes/com/test/demo/service/A.class]: Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'b' defined in file [/Users/haolonglong/IdeaProjects/project_boot/target/classes/com/test/demo/service/B.class]: Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'a': Requested bean is currently in creation: Is there an unresolvable circular reference?
2020-07-10 08:01:21.265  INFO 879 --- [           main] o.apache.catalina.core.StandardService   : Stopping service [Tomcat]
2020-07-10 08:01:21.276  INFO 879 --- [           main] ConditionEvaluationReportLoggingListener : 

Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
2020-07-10 08:01:21.278 ERROR 879 --- [           main] o.s.b.d.LoggingFailureAnalysisReporter   : 

***************************
APPLICATION FAILED TO START
***************************

Description:

The dependencies of some of the beans in the application context form a cycle:

┌─────┐
|  a defined in file [/Users/haolonglong/IdeaProjects/project_boot/target/classes/com/test/demo/service/A.class]
↑     ↓
|  b defined in file [/Users/haolonglong/IdeaProjects/project_boot/target/classes/com/test/demo/service/B.class]
└─────┘



Process finished with exit code 1

4 解决方法

4.1 重新设计

当你有一个循环依赖,很可能你有一个设计问题并且各责任没有得到很好的分离。你应该尽量正确地重新设计组件,以便它们的层次是精心设计的,也没有必要循环依赖。如果不能重新设计组件(可能有很多的原因:遗留代码,已经被测试并不能修改代码,没有足够的时间或资源来完全重新设计......),但有一些变通方法来解决这个问题。

4.2 使用 @Lazy

解决Spring 循环依赖的一个简单方法就是对一个Bean使用延时加载。也就是说:这个Bean并没有完全的初始化完,实际上他注入的是一个代理,只有当他首次被使用的时候才会被完全的初始化。

package com.test.demo.service;

import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;

@Component
public class A {

    private B b;

    public A (@Lazy B b) {
        this.b = b;
    }
}

4.3 使用 Setter/Field 注入

其中最流行的解决方法,就是Spring文档中建议,使用setter注入。简单地说,你对你须要注入的bean是使用setter注入(或字段注入),而不是构造函数注入。通过这种方式创建Bean,实际上它此时的依赖并没有被注入,只有在你须要的时候他才会被注入进来。

package com.test.demo.service;

import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;

@Component
public class A {

    private B b;

    public void setB(B b) {
        this.b = b;
    }

    public B getB() {
        return b;
    }
}

package com.test.demo.service;

import org.springframework.stereotype.Component;

import javax.annotation.Resource;

@Component
public class B {

    private A a ;

    public void setA(A a) {
        this.a = a;
    }

    public A getA() {
        return a;
    }
}

4.4 使用 @PostConstruct

打破循环的另一种方式是,在要注入的属性(该属性是一个bean)上使用 @Autowired ,并使用@PostConstruct 标注在另一个方法,且该方法里设置对其他的依赖。我们的Bean将修改成下面的代码:

4.5 实现ApplicationContextAware and InitializingBean接口

如果一个Bean实现了ApplicationContextAware,该Bean可以访问Spring上下文,并可以从那里获取到其他的bean。实现InitializingBean接口,表明这个bean在所有的属性设置完后做一些后置处理操作(调用的顺序为init-method后调用);在这种情况下,我们需要手动设置依赖。

@Component
public class CircularDependencyA implements ApplicationContextAware, InitializingBean {
 
    private CircularDependencyB circB;
 
    private ApplicationContext context;
 
    public CircularDependencyB getCircB() {
        return circB;
    }
 
    @Override
    public void afterPropertiesSet() throws Exception {
        circB = context.getBean(CircularDependencyB.class);
    }
 
    @Override
    public void setApplicationContext(final ApplicationContext ctx) throws BeansException {
        context = ctx;
    }
}

public class CircularDependencyB {
 
    private CircularDependencyA circA;
 
    private String message = "Hi!";
 
    @Autowired
    public void setCircA(CircularDependencyA circA) {
        this.circA = circA;
    }
 
    public String getMessage() {
        return message;
    }
}

同样,我们可以运行之前的测试,看看有没有异常抛出,程序结果是否是我们所期望的那样。综上,一般采用第三种setter的方式和第二种@Lazy延迟加载的策略

5 实现原理

5.1. 过程演示

关于Spring bean的创建,其本质上还是一个对象的创建,既然是对象,读者朋友一定要明白一点就是,一个完整的对象包含两部分:当前对象实例化和对象属性的实例化。在Spring中,对象的实例化是通过反射实现的,而对象的属性则是在对象实例化之后通过一定的方式设置的。这个过程可以按照如下方式进行理解:

然后我们看上面解决循环依赖注入的示例3

@Component
public class A {

    private B b;

    public void setB(B b) {
        this.b = b;
    }

    public B getB() {
        return b;
    }
}

可以看到,这里A和B中各自都以对方为自己的全局属性。这里首先需要说明的一点是,Spring实例化bean是通过ApplicationContext.getBean()方法来进行的。如果要获取的对象依赖了另一个对象,那么其首先会创建当前对象,然后通过递归的调用ApplicationContext.getBean()方法来获取所依赖的对象,最后将获取到的对象注入到当前对象中。这里我们以上面的首先初始化A对象实例为例进行讲解。

  • 首先Spring尝试通过ApplicationContext.getBean()方法获取A对象的实例,由于Spring容器中还没有A对象实例,因而其会创建一个A对象,然后发现其依赖了B对象,因而会尝试递归的通过ApplicationContext.getBean()方法获取B对象的实例。
  • Spring容器中此时也没有B对象的实例,因而其还是会先创建一个B对象的实例。需要注意这个时间点,此时A对象和B对象都已经创建了,并且保存在Spring容器中了,只不过A对象的属性b和B对象的属性a都还没有设置进去。
  • 在Spring创建B对象之后,Spring发现B对象依赖了属性A,因而此时还是会尝试递归的调用ApplicationContext.getBean()方法获取A对象的实例,因为Spring中已经有一个A对象的实例,虽然只是半成品(其属性b还未初始化),但其也还是目标bean,因而会将该A对象的实例返回。此时,B对象的属性a就设置进去了。
  • 然后还是ApplicationContext.getBean()方法递归的返回,也就是将B对象的实例返回,此时就会将该实例设置到A对象的属性b中。
  • 这个时候,注意A对象的属性b和B对象的属性a都已经设置了目标对象的实例了。读者朋友可能会比较疑惑的是,前面在为对象B设置属性a的时候,这个A类型属性还是个半成品。但是需要注意的是,这个A是一个引用,其本质上还是最开始就实例化的A对象。而在上面这个递归过程的最后,Spring将获取到的B对象实例设置到了A对象的属性b中了,这里的A对象其实和前面设置到实例B中的半成品A对象是同一个对象,其引用地址是同一个,这里为A对象的b属性设置了值,其实也就是为那个半成品的a属性设置了值。下面我们通过一个流程图来对这个过程进行讲解:

图中getBean()表示调用Spring的ApplicationContext.getBean()方法,而该方法中的参数,则表示我们要尝试获取的目标对象。图中的黑色箭头表示一开始的方法调用走向,走到最后,返回了Spring中缓存的A对象之后,表示递归调用返回了,此时使用绿色的箭头表示。从图中我们可以很清楚的看到,B对象的a属性是在第三步中注入的半成品A对象,而A对象的b属性是在第二步中注入的成品B对象,此时半成品的A对象也就变成了成品的A对象,因为其属性已经设置完成了。

5.2 源码解析

重点方法流程如下

对于Spring处理循环依赖问题的方式,我们这里通过上面的流程图其实很容易就可以理解,需要注意的一个点就是,Spring是如何标记开始生成的A对象是一个半成品,并且是如何保存A对象的。

  • 标记工作Spring是使用ApplicationContext的属性Set<String> singletonsCurrentlyInCreation来保存的,
  • 半成品的A对象则是通过Map<String, ObjectFactory<?>> singletonFactories来保存的,这里的ObjectFactory是一个工厂对象,可通过调用其getObject()方法来获取目标对象。

在AbstractBeanFactory.doGetBean()方法中获取对象的方法如下:

protected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType,
  @Nullable final Object[] args, boolean typeCheckOnly) throws BeansException {
 // 尝试通过bean名称获取目标bean对象,比如这里的A对象
 Object sharedInstance = getSingleton(beanName);
 
 // 我们这里的目标对象都是单例的
 if (mbd.isSingleton()) {
  // 这里就尝试创建目标对象,第二个参数传的就是一个ObjectFactory类型的对象,这里是使用Java8的lamada
  // 表达式书写的,只要上面的getSingleton()方法返回值为空,则会调用这里的getSingleton()方法来创建
  // 目标对象
  sharedInstance = getSingleton(beanName, () -> {
   try {
    // 尝试创建目标对象
    return createBean(beanName, mbd, args);
   } catch (BeansException ex) {
    throw ex;
   }
  });
 }
 return (T) bean;
}

这里的doGetBean()方法是非常关键的一个方法上面也主要有两个步骤,

  • 第一个步骤的getSingleton()方法的作用是尝试从缓存中获取目标对象,如果没有获取到,则尝试获取半成品的目标对象;如果第一个步骤没有获取到目标对象的实例,那么就进入第二个步骤,
  • 第二个步骤的getSingleton()方法的作用是尝试创建目标对象,并且为该对象注入其所依赖的属性。这里其实就是主干逻辑,我们前面图中已经标明,在整个过程中会调用三次doGetBean方法。
  • 第一次调用的时候会尝试获取A对象实例,此时走的是第一个getSingleton()方法,由于没有已经创建的A对象的成品或半成品,因而这里得到的是null,
  • 然后就会调用第二个getSingleton()方法,创建A对象的实例,然后递归的调用doGetBean()方法,尝试获取B对象的实例以注入到A对象中,此时由于Spring容器中也没有B对象的成品或半成品,因而还是会走到第二个getSingleton()方法,在该方法中创建B对象的实例,创建完成之后,尝试获取其所依赖的A的实例作为其属性,因而还是会递归的调用doGetBean()方法。
  • 此时需要注意的是,在前面由于已经有了一个半成品的A对象的实例,因而这个时候,再尝试获取A对象的实例的时候,会走第一个getSingleton()方法,在该方法中会得到一个半成品的A对象的实例。然后将该实例返回,并且将其注入到B对象的属性a中,此时B对象实例化完成。然后将实例化完成的B对象递归的返回,此时就会将该实例注入到A对象中,这样就得到了一个成品的A对象。

第一个getSingleton()方法

@Nullable
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
 // 尝试从缓存中获取成品的目标对象,如果存在,则直接返回
 Object singletonObject = this.singletonObjects.get(beanName);
 // 如果缓存中不存在目标对象,则判断当前对象是否已经处于创建过程中,
    //在前面的讲解中,第一次尝试获取A对象的实例之后,就会将A对象标记为正在创建中,
    //因而最后再尝试获取A对象的时候,这里的if判断就会为true
 if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
  synchronized (this.singletonObjects) {
   singletonObject = this.earlySingletonObjects.get(beanName);
   if (singletonObject == null && allowEarlyReference) {
    // 这里的singletonFactories是一个Map,其key是bean的名称,
    //而值是一个ObjectFactory类型的对象,
    //这里对于A和B而言,调用图其getObject()方法返回的就是A和B对象的实例,无论是否是半成品
    ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
    if (singletonFactory != null) {
     // 获取目标对象的实例
     singletonObject = singletonFactory.getObject();
     this.earlySingletonObjects.put(beanName, singletonObject);
     this.singletonFactories.remove(beanName);
    }
   }
  }
 }
 return singletonObject;
}

这里我们会存在一个问题就是A的半成品实例是如何实例化的,然后是如何将其封装为一个ObjectFactory类型的对象,并且将其放到上面的singletonFactories属性中的。

这主要是在前面的第二个getSingleton()方法中,其最终会通过其传入的第二个参数,从而调用

createBean()方法,该方法的最终调用是委托给了另一个doCreateBean()方法进行的,这里面有如下一段代码:

protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)
 throws BeanCreationException {

 // 实例化当前尝试获取的bean对象,比如A对象和B对象都是在这里实例化的
 BeanWrapper instanceWrapper = null;
 if (mbd.isSingleton()) {
  instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);
 }
 if (instanceWrapper == null) {
  instanceWrapper = createBeanInstance(beanName, mbd, args);
 }
 
 // 判断Spring是否配置了支持提前暴露目标bean,也就是是否支持提前暴露半成品的bean
 boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences 
  && isSingletonCurrentlyInCreation(beanName));
 if (earlySingletonExposure) {
  // 如果支持,为了解决循环依赖,再bean初始化完成前将创建实例的objectFactory加入工厂
  addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
 }

 try {
  // 在初始化实例之后,这里就是判断当前bean是否依赖了其他的bean,如果依赖了,
  // 就会递归的调用getBean()方法尝试获取目标bean
  populateBean(beanName, mbd, instanceWrapper);
 } catch (Throwable ex) {
  // 省略...
 }
 
 return exposedObject;
}

到这里,Spring整个解决循环依赖问题的实现思路已经比较清楚了。对于整体过程,读者朋友只要理解两点:

  • Spring是通过递归的方式获取目标bean及其所依赖的bean的;
  • Spring实例化一个bean的时候,是分两步进行的,首先实例化目标bean,然后为其注入属性。
  • 结合这两点,也就是说,Spring在实例化一个bean的时候,是首先递归的实例化其所依赖的所有bean,直到某个bean没有依赖其他bean,此时就会将该实例返回,然后反递归的将获取到的bean设置为各个上层bean的属性的。

5.3 三级缓存分析

对于单例来说,在Spring容器整个生命周期内,有且只有一个对象,所以很容易想到这个对象应该存在Cache中,Spring为了解决单例的循环依赖问题,使用了三级缓存。

首先我们看源码,三级缓存主要指:

/** Cache of singleton objects: bean name --> bean instance */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(256);

/** Cache of singleton factories: bean name --> ObjectFactory */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<String, ObjectFactory<?>>(16);

/** Cache of early singleton objects: bean name --> bean instance */
private final Map<String, Object> earlySingletonObjects = new HashMap<String, Object>(16);

这三级缓存分别指:

  • 一级缓存:singletonObjects:存放完全实例化属性赋值完成的Bean,直接可以使用。
  • 二级缓存:earlySingletonObjects:存放早期Bean的引用,尚未属性装配的Bean。
  • 三级缓存:singletonFactories:存放可以生成Bean的工厂,可以通过工厂获取需要的bean。

我们在创建bean的时候,首先想到的是从cache中获取这个单例的bean,这个缓存就是singletonObjects。主要调用方法就就是:getSingleton(String beanName, boolean allowEarlyReference)

分析getSingleton()的整个过程,Spring首先从一级缓存singletonObjects中获取。如果获取不到,并且对象正在创建中,就再从二级缓存earlySingletonObjects中获取。如果还是获取不到且允许singletonFactories通过getObject()获取,就从三级缓存singletonFactory.getObject()(三级缓存)获取,如果获取到了则从singletonFactories中移除,并放入earlySingletonObjects中。其实也就是从三级缓存移动到了二级缓存。

6 问题

1 为什么使用三级缓存

1 使用一级缓存

  • 实例化A -> 将半成品的A放入singletonObjects中。
  • 填充A的属性时发现取不到B->实例化B。
  • 从singletonObjects中取出A填充B的属性。
  • 将成品B放入singletonObjects->将B填充到A的属性中。
  • 将成品A放入singletonObjects。

问题:这种基本流程是通的,但是如果在整个流程进行中,有另一个线程要来取A,那么有可能拿到的只是一个属性都为null的半成品A,这样就会有问题。

2 使用二级缓存

a)使用singletonObjects和earlySingletonObjects

成品放在singletonObjects中,半成品放在earlySingletonObjects中,流程如下:

  • 实例化A ->将半成品的A放入earlySingletonObjects中。
  • 填充A的属性时发现取不到B->实例化B->将半成品的A放入earlySingletonObjects中
  • 从earlySingletonObjects中取出A填充B的属性->将成品B放入singletonObjects
  • 并从earlySingletonObjects中删除B->将B填充到A的属性中。
  • 将成品A放入singletonObjects并删除earlySingletonObjects。

问题:这样的流程是线程安全的,不过如果A上加个切面(AOP),这种做法就没法满足需求了,因为earlySingletonObjects中存放的都是原始对象,而我们需要注入的其实是A的代理对象。

b)使用singletonObjects和singletonFactories

成品放在singletonObjects中,半成品通过singletonFactories来获取,流程如下:

  • 实例化A ->创建A的对象工厂并放入singletonFactories中
  • 填充A的属性时发现取不到B->实例化B->创建B的对象工厂并放入singletonFactories中
  • 从singletonFactories中获取A的对象工厂并获取A填充到B中->将成品B放入singletonObjects并从singletonFactories中删除B的对象工厂
  • 将B填充到A的属性中->将成品A放入singletonObjects并删除A的对象工厂。

问题:这样的流程也适用于普通的IOC以及有并发的场景,但如果A上加个切面(AOP)的话,这种情况也无法满足需求。因为不可能每次执行singleFactory.getObject()方法都给我产生一个新的代理对象,所以还要借助另外一个缓存来保存产生的代理对象。

责任编辑:武晓燕 来源: 今日头条
相关推荐

2024-02-20 21:34:16

循环GolangGo

2022-08-01 07:57:03

数组操作内存

2024-02-28 08:41:51

Maven冲突版本

2022-05-26 00:19:29

通信信息5G

2022-02-23 08:41:58

NATIPv4IPv6

2024-01-15 08:41:25

SwiftTypeScrip语法

2024-03-26 07:38:16

正向代理反向代理代码

2023-04-26 07:30:00

promptUI非结构化

2022-10-08 00:00:05

SQL机制结构

2021-08-27 07:06:10

IOJava抽象

2023-08-04 08:20:56

DockerfileDocker工具

2023-09-10 21:42:31

2023-08-10 08:28:46

网络编程通信

2022-05-24 08:21:16

数据安全API

2023-06-30 08:18:51

敏捷开发模式

2023-03-29 08:26:06

2024-07-29 08:24:43

2024-09-10 08:39:48

JVMJava程序

2022-05-23 11:03:53

云原生技术DockerIstio

2021-07-31 11:40:55

Openresty开源
点赞
收藏

51CTO技术栈公众号