Spring源码之bean 的加载(三)

简介: bean 的加载(三)

bean 的加载(三)


之前文章主要讲解了从bean的实例中获取对象,准备过程以及实例化的前置处理。实例化bean是一个非常复杂的过程,本文主要讲解Spring是如何解决循环依赖。


什么是循环依赖

循环依赖就是循环引用,其实就是两个或者多个bean相互持有对方,比如 A 引用 B ,B 引用 C,C 引用 A,最终成为一个环。

image.png

循环依赖是无法解决的,除非有终结条件,否则就是死循环,直到内存溢出。


什么情况下循环依赖可以被处理

Spring中解决循环依赖是有前置条件:


  • 出现循环依赖的bean必须是单例的,如果是prototype则不会出现
  • 依赖注入的方式不能全为构造器注入,只能解决纯setter注入的情况


依赖情况 依赖注入方式 是否可以解决
A、B相互依赖 均采用setter方式注入 可以
A、B相互依赖 均采用属性自动注入 可以
A、B相互依赖 均采用构造器注入 不可以
A、B相互依赖 A中注入为setter,B中为构造器 可以
A、B相互依赖 A中注入为构造器,B中为setter,Spring在创建过程中会根据自然排序,A优先于B创建 不可以


Spring如何解决循环依赖

首先Spring容器循环依赖包括构造器循环依赖setter循环依赖,在了解Spring是如何解决循环依赖之前,我们先创建这几个类。

public class TestA {
   private TestB testB;
   public TestA(TestB testB) {
         this.testB = testB;
     }
   public void a(){
      testB.b();
   }
   public TestB getTestB() {
      return testB;
   }
   public void setTestB(TestB testB){
      this.testB = testB;
   }
}
public class TestB {
   private TestC testC;
   public TestB(TestC testC) {
          this.testC = testC;
     }
   public void b(){
      testC.c();
   }
   public TestC getTestC() {
      return testC;
   }
   public void setTestC(TestC testC) {
      this.testC = testC;
   }
}
public class TestC {
   private TestA testA;
   public TestC(TestA testA) {
          this.testA = testA;
     }
   public void c() {
      testA.a();
   }
   public TestA getTestA() {
      return testA;
   }
   public void setTestA(TestA testA) {
      this.testA = testA;
   }
}

构造器循环依赖

构造器循环依赖表示通过构造器注入造成的循环依赖,需要注意的是,这种情况是无法解决的,会抛出BeanCurrentlyInCreationException异常。


Spring容器会将每一个正在创建的Bean标识符放在一个“当前创建Bean池”中,Bean标识符在创建过程中将一直保持在这个池中。如果在创建bean的过程中发现自己已经在“当前创建Bean池”时,就会抛出上述异常表示循环依赖。当bean创建完成后则会从“当前创建Bean池”移除。


  1. 创建配置文件
<bean id="a" class="cn.jack.TestA">
   <constructor-arg index="0" ref="b"/>
</bean>
<bean id="b" class="cn.jack.TestB">
   <constructor-arg index="0" ref="c"/>
</bean>
<bean id="c" class="cn.jack.TestC">
   <constructor-arg index="0" ref="a"/>
</bean>
  1. 创建测试用例
public class Test {
    public static void main(String[] args) {
         new ClassPathXmlApplicationContext("spring-config.xml");
    }
}
  1. 运行结果
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'b' defined in class path resource [spring-config.xml]: Cannot resolve reference to bean 'c' while setting constructor argument; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'c' defined in class path resource [spring-config.xml]: Cannot resolve reference to bean 'a' while setting constructor argument; 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?


如果了解了刚才描述的情况,我们很容易就可以想到是在创建TestC对象的时候需要准备其构造参数TestA,这时候Spring容器要去创建TestA,但发现该bean标识符已经在“当前创建Bean池”中了,所以就抛出上述异常。


setter循环依赖

对于setter注入造成的依赖是通过Spring容器提前暴露刚完成构造器注入但还未完成其他步骤(比如setter注入)的bean来完成的,而且只能解决单例作用域下的bean循环依赖。通过提前暴露一个单例工厂方法,从而使其他bean可以引用到该bean。


根据我们的代码案例流程如下:


  • Spring容器创建单例TestA,随后调用无参构造器创建bean,并暴露出ObjectFactory,用于返回一个提前暴露创建中的bean,并将 testA 标识符放到 “当前创建bean池”中,然后进行setter注入 TestB
  • Spring容器创建单例TestB,随后调用无参构造器创建bean,并暴露出ObjectFactory,用于返回一个提前暴露创建中的bean,并将 testB 标识符放到 “当前创建bean池”中,然后进行setter注入 TestC
  • Spring容器创建单例TestC,随后调用无参构造器创建bean,并暴露出ObjectFactory,用于返回一个提前暴露创建中的bean,并将 testC 标识符放到 “当前创建bean池”中,然后进行setter注入 TestA ,由于之前已经提前暴露了 ObjectFactory,从而使用它返回提前暴露的一个创建中的bean。其余同理。


prototype范围的依赖处理

scope="prototype"意思是每次请求都会创建一个实例对象。


两者的区别是:有状态的bean都使用Prototype作用域,无状态的一般都使用singleton单例作用域。


对于“prototype”作用域Bean,Spring容器无法完成依赖注入,因为“prototype”作用域的Bean,Spring容器不进行缓存,因此无法提前暴露一个创建中的Bean。所以还是会抛出上述异常。


流程解析

这里就拿TestA 依赖 TestB,TestB 依赖 TestA 举例。

image.png

在 TestA 和 TestB 循环依赖的场景中:


TestB populatedBean 查找依赖项 TestA 的时候,从一级缓存中虽然未获取到 TestA,但是发现 TestA 在创建中。


此时,从三级缓存中获取 A 的 singletonFactory 调用工厂方法,创建 getEarlyBeanReference TestA 的早期引用并返回。


二级缓存能否解决循环依赖

我们知道在实例化过程中,将处于半成品的对象地址全部放在缓存中,提前暴露对象,在后续的过程中,再次对提前暴露的对象进行赋值,然后将赋值完成的对象,也就是成品对象放在一级缓存中,删除二级和三级缓存。


如果不要二级缓存的话,一级缓存会存在半成品和成品的对象,获取的时候,可能会获取到半成品的对象,无法使用。


如果不要三级缓存的话,未使用AOP的情况下,只需要一级和二级缓存也是可以解决Spring循环依赖;但是如果使用了AOP进行增强功能的话,必须使用三级缓存,因为在获取三级缓存过程中,会用代理对象替换非代理对象,如果没有三级缓存,那么就无法得到代理对象。


三级缓存是为了解决AOP代理过程中产生的循环依赖问题。


我们在代码上增加aop相关切面操作后,变化就在initializeBean方法中产生,调用applyBeanPostProcessorsBeforeInitialization方法。

image.png

image.png

在getBeanPostProcessors中中有一个处理器为:


AnnotationAwareAspectJAutoProxyCreator 其实就是加的注解切面,随后调用会跳转到 AbstractAutoProxyCreator 类的 postProcessAfterInitialization 方法中。


如下代码,wrapIfNecessary 方法会判断是否满足代理条件,是的话返回一个代理对象,否则返回当前 Bean。

image.png

最后TestA 被替换为了代理对象。在doCreateBean 返回,以及后面放到一级缓存中的都是代理对象。


如果TestA和TestB都是用了AOP动态代理,前面的一些列流程,都和正常的没有什么区别。而唯一的区别在于,创建 TestB 的时候,需要从三级缓存获取 TestA。


此时在 getSingleton 方法中会调用:singletonObject = singletonFactory.getObject();

image.pngimage.pngimage.png

image.png

看到 wrapIfNecessary 就明白了吧!这里会获取一个代理对象


也就是说此时返回,并放到二级缓存的是一个 TestA 的代理对象。


这样 TestB 就创建完毕了!


到 TestA 开始初始化并执行后置处理器了,因为 TestA 也有代理,所以 TestA 也会执行到 postProcessAfterInitialization 这一部分!


但是在执行 wrapIfNecessary 之前,会先判断代理对象缓存是否有 TestA 了。

image.png

但是这块获取到的是 TestA 的代理对象。肯定是 false 。 所以不会再生成一次 TestA 的代理对象。


总结

TestA和TestB都存在动态代理情况下的流程图:

image.png


相关文章
|
3天前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
16 2
|
19天前
|
数据采集 监控 前端开发
二级公立医院绩效考核系统源码,B/S架构,前后端分别基于Spring Boot和Avue框架
医院绩效管理系统通过与HIS系统的无缝对接,实现数据网络化采集、评价结果透明化管理及奖金分配自动化生成。系统涵盖科室和个人绩效考核、医疗质量考核、数据采集、绩效工资核算、收支核算、工作量统计、单项奖惩等功能,提升绩效评估的全面性、准确性和公正性。技术栈采用B/S架构,前后端分别基于Spring Boot和Avue框架。
|
10天前
|
缓存 Java Spring
实战指南:四种调整 Spring Bean 初始化顺序的方案
本文探讨了如何调整 Spring Boot 中 Bean 的初始化顺序,以满足业务需求。文章通过四种方案进行了详细分析: 1. **方案一 (@Order)**:通过 `@Order` 注解设置 Bean 的初始化顺序,但发现 `@PostConstruct` 会影响顺序。 2. **方案二 (SmartInitializingSingleton)**:在所有单例 Bean 初始化后执行额外的初始化工作,但无法精确控制特定 Bean 的顺序。 3. **方案三 (@DependsOn)**:通过 `@DependsOn` 注解指定 Bean 之间的依赖关系,成功实现顺序控制,但耦合性较高。
实战指南:四种调整 Spring Bean 初始化顺序的方案
|
9天前
|
前端开发 Java 开发者
Spring生态学习路径与源码深度探讨
【11月更文挑战第13天】Spring框架作为Java企业级开发中的核心框架,其丰富的生态系统和强大的功能吸引了无数开发者的关注。学习Spring生态不仅仅是掌握Spring Framework本身,更需要深入理解其周边组件和工具,以及源码的底层实现逻辑。本文将从Spring生态的学习路径入手,详细探讨如何系统地学习Spring,并深入解析各个重点的底层实现逻辑。
35 9
|
1月前
|
XML Java 数据格式
Spring从入门到入土(bean的一些子标签及注解的使用)
本文详细介绍了Spring框架中Bean的创建和使用,包括使用XML配置文件中的标签和注解来创建和管理Bean,以及如何通过构造器、Setter方法和属性注入来配置Bean。
66 9
Spring从入门到入土(bean的一些子标签及注解的使用)
|
29天前
|
Java 测试技术 Windows
咦!Spring容器里为什么没有我需要的Bean?
【10月更文挑战第11天】项目经理给小菜分配了一个紧急需求,小菜迅速搭建了一个SpringBoot项目并完成了开发。然而,启动测试时发现接口404,原因是控制器包不在默认扫描路径下。通过配置`@ComponentScan`的`basePackages`字段,解决了问题。总结:`@SpringBootApplication`默认只扫描当前包下的组件,需要扫描其他包时需配置`@ComponentScan`。
|
1月前
|
Java Spring
Spring底层架构源码解析(三)
Spring底层架构源码解析(三)
108 5
|
1月前
|
XML Java 数据格式
Spring底层架构源码解析(二)
Spring底层架构源码解析(二)
|
1月前
|
XML Java 数据格式
手动开发-简单的Spring基于注解配置的程序--源码解析
手动开发-简单的Spring基于注解配置的程序--源码解析
46 0
|
1月前
|
XML Java 数据格式
手动开发-简单的Spring基于XML配置的程序--源码解析
手动开发-简单的Spring基于XML配置的程序--源码解析
79 0