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 存在线程安全问题,主要是因为当多个线程操作同一个对象的时候是存在资源竞争的。常见的有两种解决办法:
- 在 Bean 中尽量避免定义可变的成员变量。其实,大部分 Bean 实际都是无状态(没有实例变量)的(比如 Dao、Service),这种情况下, Bean 是线程安全的。
- 在类中定义一个
ThreadLocal
成员变量,将需要的可变成员变量保存在ThreadLocal
中(推荐的一种方式)。
BeanFactory
和ApplicationContext
关系
接口间关系
-
BeanFactory
是Spring的IOC容器,负责Bean的创建与管理。 -
ApplicationContext
接口间接继承了BeanFactory
接口,并且组合了BeanFactory
。- 比如使用
ApplicationContext
的getBean
方法 = 先拿到BeanFactory
成员变量,再去调用BeanFactory
的getBean
方法
- 比如使用
-
ApplicationContext
在BeanFactory
的基础上进行了增强- Message:国际化(翻译)
- Resource:根据通配符匹配资源的能力
- Environment:获取properties文件和系统环境变量对应键的值
bean
初始化时机不同
-
BeanFactory是遵循懒加载模式创建对象,也就是说BeanFactory被创建的时候并没有初始化bean,只有getBean的时候才去创建对象。
-
ApplicationContext是遵循饿汉模式创建对象,系统创建ApplicationContext容器时,默认就会初始化所有的Bean。
- 在一开始就全部加载Bean开销较大,但是由于是单例,之后使用就能够快速得到Bean。
- 我们可以通过lazy-init设置成懒加载模式。
ApplicationContext
的三个实现类:
ClassPathXmlApplication
:把上下文文件当成类路径资源。FileSystemXmlApplication
:从文件系统中的 XML 文件载入上下文定义信息。XmlWebApplicationContext
:从 Web 系统中的 XML 文件载入上下文定义信息。
Spring bean 生命周期
Spring refresh
方法 (铺垫)
-
refresh()是 Spring 最核心的方法,没有之一。refresh 是 AbstractApplicationContext 中的一个方法,负责初始化 ApplicationContext 容器,容器必须调用 refresh 才能正常工作。
-
它的内部主要会调用 12 个方法,我们把它们称为 refresh 的 12 个步骤:
- 注意下面的
Environment
和BeanFactory
都是ApplicationContext
的成员变量。
- 注意下面的
总的来说,初始化ApplicationContext容器就是初始化ApplicationContext的成员变量。
Environment
初始化BeanFactory
初始化成员变量(bean definiton、解析SpEL器、特殊bean解析器、注册bean后处理器),调用BeanFactory后处理器来补充bean definiton。ApplicationContext
初始化成员变量(国际化、事件广播器、事件监听器,生命周期管理器),初始化单例Bean,并执行Bean后处理器扩展。
Bean 生命周期
- 总体流程:\(创建 {\rightarrow} 依赖注入 {\rightarrow} 初始化 {\rightarrow} 使用期 {\rightarrow} 销毁\)
创建
如何从配置中创建出一个bean?
- 程序启动后,
ApplicationContext
会调用refresh
初始化方法。- 将配置文件或配置类定义好的bean转换成BeanDefinition对象存入
BeanDefinitionMap
- 之后,
refresh
方法会向下调用根据BeanDefinitionMap
初始化所有非延迟的单例bean。
- 将配置文件或配置类定义好的bean转换成BeanDefinition对象存入
- 执行到
doGetBean
方法- 先调用
getsingleton()
尝试从三级缓存
中获取bean实例。如果获取到了,完整的bean实例直接返回,创建中的bean说明可能有循环依赖,FactoryBean通过其getObject
获取Bean实例。 - 接着向上递归获取
父容器
,尝试找到该beanName的对象实例。这里有点像类加载时的双亲委派机制去加载bean。 - 如果未获取到,进入真正创建bean的逻辑。
- 先调用
- 执行到
doCreateBean
方法,先根据构造方法创建出一个bean对象。createBeanInstance()
依赖注入
执行到populateBean
方法,Spring根据BeanDefinition
中的信息进行依赖注入。比如:
要点 | 总结 |
---|---|
AutowiredAnnotationBeanPostProcessor | 识别 @Autowired 及 @Value 标注的成员,封装为 InjectionMetadata 进行依赖注入 |
CommonAnnotationBeanPostProcessor | 识别 @Resource 标注的成员,封装为 InjectionMetadata 进行依赖注入。它还能处理@PostConstruct和@PreDestroy |
初始化
执行initializeBean
方法
invokeAwareMethod
:调用Aware接口相关的方法,给Bean设置beanName, beanFactory,beanClassLoader,。BeanPostProcessorsBeforeInitialization
:调用Bean后置处理器在初始化前的方法,如@PostConstruct
,该注解由CommonAnnotationBeanPostProcessor
解析invokeInitMethod
:初始化方法,按序执行InitializingBean的接口方法,自定义的initMethod方法。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()。
- 判断并登记可销毁 bean
- 判断依据:
- 如果实现了 DisposableBean 或 AutoCloseable 接口,则为可销毁 bean
- 如果自定义了 destroyMethod或有 @PreDestroy 标注的方法,则为可销毁 bean
- 存储时都会封装为 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。
三级缓存存放内容
- 一级缓存:一级缓存就是我们常常说的spring的单例容器,spring依赖注入的bean都是从这个单例池中去获取的,创建后完整的bean也是存放在这个单例池中的,我们这里叫它为一级缓存;
- 二级缓存:可能不完整的原始对象或者代理对象
- 三级缓存:存放的是我们的实例化后的原始对象,就是存放一个刚刚被创建出来的原始对象。
1. 只有在循环依赖 & AOP代理的情况下,才会提前创建代理对象
2. 如果提前创建代理对象,初始化时则不会创建代理对象,获取时直接从二级缓存获取
1.如果A开启了AOP代理:
a.如果A出现了循环依赖,那么在填充属性的时候就会去创建A的代理对象(getEarlyBeanReference)放入二级缓存,那么A在实例化过后发现A已经创建了代理对象,所以就不会去创建aop代理对象,;
b.如果A没有出现循环依赖,那么填充属性的时候放入二级缓存的就是普通的对象,和之前的实例化出来的原始对象一致。
2.如果a没有开启AOP代理,最后放入二级缓存的也是之前的原始对象;
解决 set 循环依赖的原理
如果只有一级缓存?
-
作用是保证单例对象仅被创建一次
- 第一次走
getBean("a")
流程后,最后会将成品 a 放入 singletonObjects 一级缓存 - 后续再走
getBean("a")
流程时,先从一级缓存中找,这时已经有成品 a,就无需再次创建
- 第一次走
-
问题:循环依赖时,A -> B -> A,此时singletonObjects 一级缓存内没有成品的 a,陷入死循环
如果只有二级缓存
解决思路如下:
- 再增加一个 singletonFactories 缓存
- 在依赖注入前,即
a.setB()
以及b.setA()
将 a 及 b 的半成品对象(未完成依赖注入和初始化)放入此缓存 - 执行依赖注入时,先看看 singletonFactories 缓存中是否有半成品的对象,如果有拿来注入,顺利走完流程。
问题:二级缓存无法正确处理循环依赖并且包含有代理创建的场景,分析如下
- spring 默认要求,在
a.init
完成之后才能创建代理pa = proxy(a)
- 由于 a 的代理创建时机靠后,在执行
factories.put(a)
向 singletonFactories 中放入的还是原始对象 - 接下来箭头 3、4、5 这几步 b 对象拿到和注入的都是原始对象
三级缓存
简单分析的话,只需要将代理的创建时机放在依赖注入之前即可,但 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 工厂间接访问