Loading

Spring IOC

😉 本文共3714字,阅读时间约10min

Spring Bean

什么是Spring IoC

IoC 的思想就是将原本在程序中手动创建对象的控制权,交由 Spring 框架来管理。

将对象之间的相互依赖关系交给 IoC 容器来管理,并由 IoC 容器完成对象的注入(这也是IOC解决的问题)。这样可以很大程度上简化应用的开发,把应用从复杂的依赖关系中解放出来。

DI—Dependency Injection,即依赖注入:组件之间依赖关系由容器在运行期决定,形象的说,即由容器动态的将某个依赖关系注入到组件之中。

bean的作用域有哪些?

Spring 中 Bean 的作用域通常有下面几种:

  • singleton : IoC 容器中只有唯一的 bean 实例。Spring 中的 bean 默认都是单例的,是对单例设计模式的应用。
  • prototype : 每次获取都会创建一个新的 bean 实例。也就是说,连续 getBean() 两次,得到的是不同的 Bean 实例。
  • request (仅 Web 应用可用): 每一次 HTTP 请求都会产生一个新的 bean(请求 bean),该 bean 仅在当前 HTTP request 内有效。
  • session (仅 Web 应用可用) : 每一次来自新 session 的 HTTP 请求都会产生一个新的 bean(会话 bean),该 bean 仅在当前 HTTP session 内有效。
  • 那什么时候用单例?什么时候用多例?
    • 当对象含有可改变状态时(在实际应用中该状态会改变),则多例,否则单例。例如dao和service层的数据一般不会有响应的属性改变,所以考虑单例,而controller层会存储很多需要操作的vo类,此时这个对象的状态就会被改变,则需要使用多例
  • 如何配置bean 的作用域呢?
    • xml或注解配置@Scope()属性
  • 单例 Bean 的线程安全问题了解吗?
    • 单例 Bean 存在线程安全问题,主要是因为当多个线程操作同一个对象的时候是存在资源竞争的。常见的有两种解决办法:

      1. 在 Bean 中尽量避免定义可变的成员变量。其实,大部分 Bean 实际都是无状态(没有实例变量)的(比如 Dao、Service),这种情况下, Bean 是线程安全的。
      2. 在类中定义一个 ThreadLocal 成员变量,将需要的可变成员变量保存在 ThreadLocal 中(推荐的一种方式)

BeanFactoryApplicationContext关系

接口间关系

  • BeanFactory是Spring的IOC容器,负责Bean的创建与管理。

  • ApplicationContext接口间接继承BeanFactory接口,并且组合BeanFactory

    • 比如使用ApplicationContextgetBean方法 = 先拿到BeanFactory成员变量,再去调用BeanFactorygetBean方法
  • ApplicationContextBeanFactory的基础上进行了增强

    • Message:国际化(翻译)
    • Resource:根据通配符匹配资源的能力
    • Environment:获取properties文件和系统环境变量对应键的值

image-20230205181408197

bean初始化时机不同

  • BeanFactory是遵循懒加载模式创建对象,也就是说BeanFactory被创建的时候并没有初始化bean,只有getBean的时候才去创建对象

  • ApplicationContext是遵循饿汉模式创建对象,系统创建ApplicationContext容器时,默认就会初始化所有的Bean

    • 在一开始就全部加载Bean开销较大,但是由于是单例,之后使用就能够快速得到Bean。
    • 我们可以通过lazy-init设置成懒加载模式。

ApplicationContext 的三个实现类:

  1. ClassPathXmlApplication:把上下文文件当成类路径资源。
  2. FileSystemXmlApplication:从文件系统中的 XML 文件载入上下文定义信息。
  3. XmlWebApplicationContext:从 Web 系统中的 XML 文件载入上下文定义信息。

Spring bean 生命周期

Spring refresh方法 (铺垫)

  • refresh()是 Spring 最核心的方法,没有之一。refresh 是 AbstractApplicationContext 中的一个方法,负责初始化 ApplicationContext 容器,容器必须调用 refresh 才能正常工作。

  • 它的内部主要会调用 12 个方法,我们把它们称为 refresh 的 12 个步骤:

    • 注意下面的EnvironmentBeanFactory都是ApplicationContext的成员变量。

image-20230205190318003

总的来说,初始化ApplicationContext容器就是初始化ApplicationContext的成员变量

  1. Environment初始化
  2. BeanFactory初始化成员变量(bean definiton、解析SpEL器、特殊bean解析器、注册bean后处理器),调用BeanFactory后处理器来补充bean definiton。
  3. ApplicationContext初始化成员变量(国际化、事件广播器、事件监听器,生命周期管理器),初始化单例Bean,并执行Bean后处理器扩展

Bean 生命周期

  • 总体流程:\(创建 {\rightarrow} 依赖注入 {\rightarrow} 初始化 {\rightarrow} 使用期 {\rightarrow} 销毁\)

img

创建

如何从配置中创建出一个bean?

  1. 程序启动后,ApplicationContext会调用refresh初始化方法。
    1. 将配置文件或配置类定义好的bean转换成BeanDefinition对象存入BeanDefinitionMap
    2. 之后,refresh方法会向下调用根据BeanDefinitionMap初始化所有非延迟的单例bean。
  2. 执行到doGetBean方法
    1. 先调用getsingleton()尝试从三级缓存中获取bean实例。如果获取到了,完整的bean实例直接返回,创建中的bean说明可能有循环依赖,FactoryBean通过其getObject获取Bean实例。
    2. 接着向上递归获取父容器,尝试找到该beanName的对象实例。这里有点像类加载时的双亲委派机制去加载bean。
    3. 如果未获取到,进入真正创建bean的逻辑。
  3. 执行到doCreateBean方法,先根据构造方法创建出一个bean对象。createBeanInstance()

依赖注入

执行到populateBean方法,Spring根据BeanDefinition中的信息进行依赖注入。比如:

要点 总结
AutowiredAnnotationBeanPostProcessor 识别 @Autowired 及 @Value 标注的成员,封装为 InjectionMetadata 进行依赖注入
CommonAnnotationBeanPostProcessor 识别 @Resource 标注的成员,封装为 InjectionMetadata 进行依赖注入。它还能处理@PostConstruct和@PreDestroy

初始化

执行initializeBean方法

  1. invokeAwareMethod:调用Aware接口相关的方法,给Bean设置beanName, beanFactory,beanClassLoader,。
  2. BeanPostProcessorsBeforeInitialization:调用Bean后置处理器在初始化前的方法,如@PostConstruct,该注解由CommonAnnotationBeanPostProcessor 解析
  3. invokeInitMethod初始化方法,按序执行InitializingBean的接口方法,自定义的initMethod方法。
  4. BeanPostProcessorsAfterInitialization调用Bean后置处理器在初始化后的方法,如创建AOP代理,由AnnotationAwareAspectJAutoProxyCreator 后置处理器创建。

一些扩展方法

  • 调用Aware 接口相关方法:让 Bean 能拿到容器的一些资源,例如 BeanNameAware 的 setBeanName(),获取ApplicationContext对象、获取当前bean的BeanFactory实例、Bean实例的名字。

    一般指具备由Spring 容器通过Aware回调方法通知的特定的bean对象,简单来说就是可以通过实现该接口拿到Spring容器内部的一些资源。

  • bean后处理器:在bean生命周期各阶段进行一些前置和后置的处理,例如 postProcessBeforeInitialization() 和 postProcessAfterInitialization();

  • 生命周期接口:定义初始化方法和销毁方法的,例如 InitializingBean 的 afterPropertiesSet(),以及 DisposableBean 的 destroy();

    • 配置生命周期方法:可以通过配置文件,自定义初始化和销毁方法,例如配置文件配置的 init() 和 destroyMethod()。
  1. 判断并登记可销毁 bean
    1. 判断依据:
      1. 如果实现了 DisposableBean 或 AutoCloseable 接口,则为可销毁 bean
      2. 如果自定义了 destroyMethod或有 @PreDestroy 标注的方法,则为可销毁 bean
    2. 存储时都会封装为 DisposableBeanAdapter 类型对销毁方法的调用进行适配

使用期

……

销毁

destroyBeans()方法

  • 销毁时机:

    • singleton bean 的销毁在 ApplicationContext.close 时,此时会找到所有 DisposableBean 的名字,逐一销毁
    • 自定义 scope bean 的销毁在作用域对象生命周期结束时
    • prototype bean 的销毁可以通过自己手动调用 destroyBean 方法执行销毁
  • 同一 bean 中不同形式销毁方法的调用次序:

    • 优先后处理器销毁,即 @PreDestroy
    • 其次 DisposableBean 接口销毁
    • 最后 destroyMethod 销毁(包括自定义名称,推断名称,AutoCloseable 接口 多选一)

总结

  • refresh()、doGetBean()

  • doCreateBean():这个是入口;createBeanInstance():用来初始化 Bean,里面会调用对象的构造方法;

  • populateBean():属性对象的依赖注入,以及成员变量初始化;

  • initializeBean():初始化Bean

  • destroyBeans():调用Bean的销毁方法

bean后处理器:

  • 创建前后的增强

  • 依赖注入前的增强:如 @Autowired、@Value、@Resource

  • 初始化前后的增强

    • Before:@PostConstruct
    • After:代理增强
  • 销毁之前的增强:@PreDestroy

Spring Bean 循环依赖 & 三级缓存

Spring 三级缓存数据结构

在这里插入图片描述

一级缓存(单例池)使用的是ConcurrentHashMap,二级缓存和三级缓存都是用的HashMap。

  • 如何查找一个Bean?
    • 简单来说,先从单例池中查找,找到直接使用。没有找到,从二级缓存中找,找到直接使用没有找到,从三级缓存中找,执行lambda,得到对象放入二级缓存。
  • 为什么二级、三级不用ConcurrentHashMap呢?
    • 需要把三级缓存中的lambda表达式执行的结果放入二级缓存,要保证两个map的put和remove操作的原子性。显然ConcurrentHashMap无法保证。
    • 只能添加synchronize加锁控制,已经保证了并发的操作安全,所以就没有必要设置为ConcurrentHashMap,在这种前提下,考虑性能,选择了HashMap。

image-20230205222722994

三级缓存存放内容

  • 一级缓存:一级缓存就是我们常常说的spring的单例容器,spring依赖注入的bean都是从这个单例池中去获取的,创建后完整的bean也是存放在这个单例池中的,我们这里叫它为一级缓存;
  • 二级缓存:可能不完整的原始对象或者代理对象
  • 三级缓存:存放的是我们的实例化后的原始对象,就是存放一个刚刚被创建出来的原始对象。

1. 只有在循环依赖 & AOP代理的情况下,才会提前创建代理对象

2. 如果提前创建代理对象,初始化时则不会创建代理对象,获取时直接从二级缓存获取

1.如果A开启了AOP代理:
a.如果A出现了循环依赖,那么在填充属性的时候就会去创建A的代理对象(getEarlyBeanReference)放入二级缓存,那么A在实例化过后发现A已经创建了代理对象,所以就不会去创建aop代理对象,;
b.如果A没有出现循环依赖,那么填充属性的时候放入二级缓存的就是普通的对象,和之前的实例化出来的原始对象一致。
2.如果a没有开启AOP代理,最后放入二级缓存的也是之前的原始对象;

image-20230205225057763

解决 set 循环依赖的原理

如果只有一级缓存?

  • 作用是保证单例对象仅被创建一次

    • 第一次走 getBean("a") 流程后,最后会将成品 a 放入 singletonObjects 一级缓存
    • 后续再走 getBean("a") 流程时,先从一级缓存中找,这时已经有成品 a,就无需再次创建
  • 问题:循环依赖时,A -> B -> A,此时singletonObjects 一级缓存内没有成品的 a,陷入死循环

如果只有二级缓存

image-20210903101849924

解决思路如下:

  • 再增加一个 singletonFactories 缓存
  • 在依赖注入前,即 a.setB() 以及 b.setA() 将 a 及 b 的半成品对象(未完成依赖注入和初始化)放入此缓存
  • 执行依赖注入时,先看看 singletonFactories 缓存中是否有半成品的对象,如果有拿来注入,顺利走完流程。

问题:二级缓存无法正确处理循环依赖并且包含有代理创建的场景,分析如下

  • spring 默认要求,在 a.init 完成之后才能创建代理 pa = proxy(a)
  • 由于 a 的代理创建时机靠后,在执行 factories.put(a) 向 singletonFactories 中放入的还是原始对象
  • 接下来箭头 3、4、5 这几步 b 对象拿到和注入的都是原始对象

image-20210903103030877

三级缓存

image-20210903103628639

简单分析的话,只需要将代理的创建时机放在依赖注入之前即可,但 spring 仍然希望代理的创建时机在 init 之后,只有出现循环依赖时,才会将代理的创建时机提前。所以解决思路稍显复杂:

  • 图中 factories.put(fa) 放入的既不是原始对象,也不是代理对象而是工厂对象 fa
  • 当检查出发生循环依赖时,fa 的产品就是代理 pa,没有发生循环依赖,fa 的产品是原始对象 a(提前创建代理
  • 假设出现了循环依赖,拿到了 singletonFactories 中的工厂对象,通过在依赖注入前获得了 pa,红色箭头 5
  • 这回 b.setA() 注入的就是代理对象,保证了正确性,红色箭头 7
  • 还需要把 pa 存入新加的 earlySingletonObjects 缓存,红色箭头 6
  • a.init 完成后,无需二次创建代理,从哪儿找到 pa 呢?earlySingletonObjects 已经缓存,蓝色箭头 9

当成品对象产生,放入 singletonObject 后,singletonFactories 和 earlySingletonObjects 就中的对象就没有用处,清除即可

如果整个过程中没有出现循环依赖,那么永远不会使用到二级缓存,而且三级缓存也只是存放了实例化后封装的对象,也不会去使用;

构造方法与多例循环依赖的解决方案

骗一下

  • 思路1 @Lazy
    • a 注入 b 的代理对象,这样能够保证 a 的流程走通
    • 后续需要用到 b 的真实对象时,可以通过代理间接访问
  • 思路2 ObjectFactory
    • a 注入 b 的工厂对象,让 b 的实例创建被推迟,这样能够保证 a 的流程先走通
    • 后续需要用到 b 的真实对象时,再通过 ObjectFactory 工厂间接访问
posted @ 2023-02-06 12:33  iterationjia  阅读(133)  评论(0编辑  收藏  举报