[Spring] Spring Bean

Details in Bean Life Circle

Posted by Penistrong on March 31, 2023

SPring Bean

Spring框架提供的两大核心功能就是IOC(Inversion Of Control)控制反转和AOP(Aspect Oriented Programming)面向切面编程

Spring作为轻量级的开发框架,集成了许多模块,其存在的使命就是为了提高程序员的开发效率,而IOC和AOP的实现都离开不了Spring对Bean的管理

Bean被创建后交由Spring的IOC容器进行管理,而AOP这个面向切面编程的思想也离不开IOC容器: 切面处的代理对象是在创建目标对象对应的Bean的时候一同生成的,尔后这个代理对象也会作为Bean加入IOC容器中,在需要依赖注入的其他Bean里优先注入作为代理对象的Bean

Spring Bean的生命周期

  1. Spring启动时扫描Bean的定义,注册Bean:

    • Spring扫描配置文件(XML方式)、指定包路径(@ComponentScan注解,如果没有的话默认扫描启动类所在包路径)下的所有.class字节码文件里被Bean相关注解(@Service@Controller@Repository@Component)注解了的类

    • 根据includeFilter@Conditional等注解判断相关类是否应该被解析为Bean

    • 利用AnnotationBeanNameGenerator生成BeanName(@Component注解可以配置BeanName参数,没有的话默认按照类名首字母小写生成,如果类名两个字母大写则Bean的名字与类名一致),最后将类上使用的其他注解赋值给BeanDefinition

    • 最后将BeanNameBeanDefinition存入BeanFactorybeanDefinitionMap<BeanName, BeanDefinition>中,完成注册

  2. Spring开始生成Bean的原始对象:

    • 利用BeanFactory获取beanDefinitionMap,根据BeanName得到其对应的BeanDefinition

    • 根据Bean的类信息,推断构造方法: 优先使用@AutoWired注解的构造方法,如果没有的话默认使用无参构造方法(如果这个类只定义了一个有参构造方法,则使用该方法)

    • 利用Java提供的反射API,根据推断出的构造方法实例化一个对象,称作原始对象

  3. 对原始对象进行依赖注入,填充原始对象中的属性:

    • 属性值利用set()方法填充

    • 涉及到其他Bean(比如有@AutoWired要求Spring自动注入的其他Bean),首先ByType再ByName去IOC容器(单例池)中取出对应的Bean对象执行注入,如果不存在则去创建目标Bean,详见Bean的依赖注入过程一节

    当然这其中会出现循环依赖问题:A Bean 依赖 B Bean,B Bean 依赖 A Bean,创建A Bean时触发B Bean的创建,然而A Bean也正在创建之中 Spring利用三级缓存解决了循环依赖问题,详见Bean的循环依赖一节

  4. 处理Aware回调,若该类实现了*Aware接口,调用对应的set*()方法:

    比如BeanNameAwareBeanClassLoaderAwareBeanFactoryAware这三个接口,分别对应BeanName、ClassLoader对象实例、BeanFactory对象实例

  5. 初始化前(当前上下文中存在BeanPostProcessor对象,可以有很多个),进行BeanPostProcessor的前置处理

    执行postProcessBeforeInitialization()方法

  6. 初始化:

    • 如果Bean实现了InitializingBean接口(利用instanceof判断),执行afterPropertiesSet()方法

    • 配置文件中Bean的属性里指定了init-method()方法或者@Bean(initMethod="")给定了初始化方法,则去执行对应的initMethod()

  7. 初始化后(存在BeanPostProcessor对象),进行BeanPostProcessor的后置处理

    执行postProcessAfterInitialization()方法,Spring AOP就是在该初始化后阶段利用AnnotationAwareAspectJAutoProxyCreator根据Bean的原始对象生成代理对象,同时代理对象里通过组合的方式引用了原始对象,保证原始对象不会被垃圾回收

  8. 如果AOP接管了初始化后阶段,那么最终放入单例池的只是这个代理对象,否则放入的就是原始对象(在postProcessAfterInitialization()方法中其实可以完全替换掉对应的bean对象,因为返回值是任意对象)

  9. 正常使用,需要该Bean的时候由IOC自动注入该Bean

  10. Spring应用上下文关闭时,IOC容器准备关闭,要销毁其中的Bean实例:

    • 如果Bean实现了DisposableBean接口,执行实现的destory()方法

    • 配置文件中Bean的属性里指定了destroy-method()方法或者@Bean(destroyMethod="")给定了销毁方法,则去执行对应的destoryMethod()

Bean的依赖注入过程

Bean的循环依赖问题与解决方式

Bean的循环依赖问题是指,在初始化Bean A走到依赖注入的步骤时,发现其需要注入Bean B,但是单例池中并没有该Bean B,于是先去创建Bean B,但是Bean B 也依赖于Bean A,而后者还没有初始化完毕,从而导致循环依赖问题

Spring利用了三级缓存singletonObjectsearlySingletonObjectssingletonFactories解决该问题

为什么是三级缓存而不是二级缓存?

如果只从IOC的角度上考虑,只需要在单例池这个一级缓存之外再添加一个二级缓存earlySingletonObjects即可:即,在实例化Bean A的原始对象后,就将其加入二级缓存中,在对Bean B进行依赖注入时,如果单例池没有初始化完毕的Bean A,就去二级缓存中获取Bean A原始对象的引用,完成依赖注入过程

考虑到Spring的另一大核心功能AOP,由于AOP是在初始化后阶段对原始对象进行代理,利用JDK动态代理或者CGLIB生成代理对象后,并让原始对象以组合的方式作为代理对象的字段之一

这样就会出现一个问题,如果二级缓存中存放的是还未初始化完成(提前暴露)的原始对象,那么Bean B在初始化时如果直接使用Bean A的原始对象,那么Bean A被AOP所处理的增强逻辑就不会被Bean B执行(Bean B执行方法时用的是Bean A的原始对象而不是代理对象)

所以Spring需要引入第三级缓存singletonFactories解决AOP的问题

三级缓存处理过程

假设Bean A依赖Bean B,而Bean A的部分方法被AOP代理(比如@Transactional等)

在实例化Bean A的原始对象后,Spring会构造一个ObjectFactory存入第三级缓存singletonFactories中,以<BeanName, ObjectFactory>键值对形式存入Map中

ObjectFactory是一个函数式接口,其中定义的getObject()方法可以传入一个Lambda表达式,在Spring的处理中是传入() -> getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean)这个Lambda表达式作为该工厂接口的实现

getEarlyBeanReference()这个方法在SmartInstantiationAwareBeanPostProcessor接口中被定义,而整个Spring中只有AbstractAutoProxyCreator类实现了该方法,Spring AOP的处理类就是AnnotationAwareAspectJAutoProxyCreator,该类的父类就是AbstractAutoProxyCreator

// in AbstractAutoProxyCreator
private final Map<Object, Object> earlyProxyReferences = new ConcurrentHashMap(16);

@Override
public Object getEarlyBeanReference(Object bean, String beanName) {
   Object cacheKey = this.getCacheKey(bean.getClass(), beanName);
   this.earlyProxyReferences.put(cacheKey, bean);
   return wrapIfNecessary(bean, beanName, cacheKey);
}

在依赖注入时,发现所需的Bean B还没有创建,于是先去初始化Bean B,后者的依赖注入过程中需要用到Bean A(由AOP的逻辑,这里注入的应该是Bean A的代理对象),检查一级缓存singletonObjects和二级缓存earlySingletonObjects中是否有Bean A的实例。如果都没有,就去第三级缓存singletonFactories根据BeanName获取对应的ObjectFactory,执行该工厂类的getObject()方法就会调用传入的Lambda表达式里的getEarlyBeanReference()方法,提前对Bean A的原始对象进行AOP代理,返回代理对象并将其放到二级缓存earlySingletonObjects,该代理对象内部持有对原始对象的引用,但是该原始对象还未完成依赖注入过程,因此不能将该代理对象直接丢到一级缓存singletonObjects

尔后,Bean B或者依赖了Bean A的其他Bean就可以直接从二级缓存earlySingletonObjects中得到Bean A原始对象的代理对象,完成依赖注入过程,最终Bean B被存储到一级缓存singletonObjects

Bean B创建完毕后,继续Bean A的依赖注入阶段,原始对象完成属性填充。尔后,循序渐进到“初始化后”阶段时,负责AOP处理的AnnotationAwareAspectJAutoProxyCreator(BeanPostProcessor)调用其父类AbstractAutoProxyCreatorpostProcessAfterInitialization()方法,判断Bean A的BeanName是否已存在于AbstractAutoProxyCreator内部的earlyProxyReferences表中,若存在则说明在之前的循环依赖解决过程中已经提前进行过AOP,无需再次进行AOP,直接跳过不处理

// in AbstractAutoProxyCreator
public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
   if (bean != null) {
      Object cacheKey = this.getCacheKey(bean.getClass(), beanName);
      if (this.earlyProxyReferences.remove(cacheKey) != bean) {
            return this.wrapIfNecessary(bean, beanName, cacheKey);
      }
   }

   return bean;
}

最后,Bean A走完整个创建周期后(所有的BeanPostProcessor都执行完毕),,此时Bean A的代理对象是存放在二级缓存earlySingletonObjects中,从二级缓存取出代理对象放入一级缓存中,解决了整个循环依赖问题

Bean的线程安全问题

线程安全问题是从JVM运行时考量的,虽然大部分Bean都是以单例模式存在,但是说到底Bean只是一个概念,实际还是一个受JVM管理的对象,对于这种单一对象自然也会存在并发情况下的线程安全问题

但是,大部分Bean是无状态的(比如Controller、Serivce、DAO等),即内部没有任何可变的成员变量,只有各种方法体,这个时候无所谓线程安全问题

如果定义了可变成员变量,就有可能会出现线程安全问题,比如多个线程在Bean中方法里读写该变量,建议是将类似的变量加入到ThreadLocal中,让每个线程从线程私有的ThreadLocalMap里取出变量读写即可

为什么Spring Bean默认是单例的

首先,Spring中Bean的作用域默认只有singletonprototype两种,在基于Spring Web的ApplicationContext下还有另外4种:requestsessionglobal-sessionwebsocket

一般来说,无状态的Bean都是单例singleton作用域的,而有状态Bean如果不采取ThreadLocal的话那就配置为原型prototype作用域,每次获取都会创建一个新的Bean实例

Bean默认单例的好处:

  • 减少对象的创建次数,Spring框架创建Bean的实例是利用Java反射API构造的,如果每次都创建新的Bean实例,反射效率比较低导致整体性能下降

  • 程序整个生命周期里创建的对象就比较少,而且单例Bean可以存活很久,减少GC次数,降低STW的时间,提高了性能。单例Bean随着分代年龄的增长,最终可以晋升到老年代里,一直存在

  • IOC容器singletonObjects就是个单例池,使用Bean时直接在单例池里根据BeanName快速获取已经创建过的Bean单例

Bean默认单例的缺点:

有状态Bean存在线程不安全问题,解决方法上一节已有