写点什么

深入了解 Spring 篇之 BeanDefinition 结构

作者:邱学喆
  • 2022 年 8 月 07 日
  • 本文字数:7646 字

    阅读完需:约 25 分钟

深入了解 Spring篇之BeanDefinition结构

一. 概述

BeanDefinition 是定义对 Bean 的接口是 spring 容器中最重要的一个接口,spring 围绕这个接口进行对象的创建以及对象中的属性注入。

  • AbstractBeanDefinition 是整个 BeanDefinition 接口的抽象类,也是具体实现类的基础类,里面的属性内容基本含盖了 spring 的 IOC 的功能特点;

  • RootBeanDefinition 在 spring 初始化 bean 实例过程所使用的复合对象;

  • ScannedGenericBeanDefinition

  • ConfigurationClassBeanDefinition 针对 classpath 解析用途

  • AnnotatedGenericBeanDefinition

  • GenericBeanDefinition

只是简单对其进行介绍其用途,但下文并不是对照本宣科的对其进行详细介绍;而是从零开始设计一套 IOC 的角度出发进行解读 BeanDefinition 结构,这样子更加对其原理以及设计思想更加了解,后续使用时就不再是陌生的;

相关 spring 源代码是 5.1.2.RELEASE 版本;

二. 对象实例化

2.1 构建对象

在常规开发中,如果要创建对象,有如下方式:

  • 通过{静态}工厂方法进行创建,例如:

public class Factory{  public {static} Object<?> createBean(){  	//....  }}
复制代码
  • 通过构造方法进行创建,例如

public class Test {    public static void main(String [] args) throws Exception {        Constructor<Person> constructor = Person.class.getConstructor();        Person man = constructor.newInstance();        System.out.println(man);    }    public static class Person{        private String name;
public Person() { }
@Override public String toString() { return "Person{" + "name='" + name + '\'' + '}'; } }}
复制代码
  • 通过 Supplier 方式进行创建,例如:

Supplier<T> instanceSupplier = new Supplier(){	public T get(){    //....  }}
复制代码
  • 通过 cglib 方式进行创建增强版对象。

除 Supplier 外,工厂方法和构造方法都是通过反射调用 Method 对象进行对象创建。所以 spring 提供一个接口 InstantiationStrategy,其将调用 Method 对象进行对象创建的逻辑封装到 SimpleInstantiationStrategy 实现类里面;

那么 BeanDefinition 结构中就包含了上面的几个创建对象的内容;

public abstract class AbstractBeanDefinition {	private volatile Object beanClass;  /**   * Supplier方式   */	private Supplier<?> instanceSupplier;  /**   * 工厂方法进行创建   */  private String factoryBeanName;  private String factoryMethodName;}
复制代码

1. 无参数构建对象

既然提供如上方式进行对象创建,那么意味着会有优先级;那么我们就可以设计一个简单的流程(无参数的方法),对目标类型进行实例化对象,如图所示:

2. 有入参构建对象

在无参构建对象过程基础上,添加这么一个逻辑,【通过指定规则从中选出目标方法】,如下图;

指定规则如下:

匹配方法入参类型与入参对象类型的逻辑。如果都不匹配,意味着找不到合适的方法,直接报错;如果有匹配的方法有2个以上,那我们就需要决策了,具体需要哪种策略了;目前有两种策略,如下:1. 宽松策略:优先选择第一个方法;2. 严格策略:再次精细化类型匹配,根据类型的继承层级来判断,层级越低,说明类型关系越接近,则优先选择关系越接近的方法;ps. 具体的实现在ArgumentsHolder类
复制代码

具体采用哪种策略,直接存放到 BeanDefinition 中;

public abstract class AbstractBeanDefinition {  /**   * true代表是采用严格策略;   * false代表是采用宽松策略   */	private boolean lenientConstructorResolution = true;}
复制代码

上面的过程中,通过反射来获取的方法列表,这里有两个可能性,是只获取 public 方法,还是获取所有的方法;

public abstract class AbstractBeanDefinition {  /**   * true代表获取所有方法;   * false代表是只获取public方法   */	private boolean nonPublicAccessAllowed = true;}
复制代码

接下来介绍如何将 ConstructorArgumentValues 解析成 ArgumentsHolder 对象的;

接下来对入参解析进行介绍,

public abstract class AbstractBeanDefinition {  /**   * 构建对象的入参对象   */	private ConstructorArgumentValues constructorArgumentValues;}
public class ConstructorArgumentValues { private Map<Integer/*参数位置*/, ValueHolder/*参数对象*/> indexedArgumentValues; private List<ValueHolder> genericArgumentValues;}
public class ValueHolder{ /** * 原始值对象 */ private Object value; /** * 是否已经转换过了,也就是意味着是否已经解析过了 */ private boolean converted = false; /** * 解析后的对象 */ private Object convertedValue;}
public class ArgumentsHolder { public final Object[] arguments;}
复制代码

转换的流程如下:


阶段一:旧ConstructorArgumentValues对象转成新ConstructorArgumentValues对象遍历旧ConstructorArgumentValues中的ValueHolder对象,经过spring解析得到新的ValueHolder对象PS. 主要的实现代码BeanDefinitionValueResolver.resolveValueIfNecessary方法中。值得注意的是,这里有用到EL表达式;所以,一些灵活得到入参对象的,可以通过EL表达式来抉择;阶段二:新ConstructorArgumentValues对象解析成ArgumentsHolder遍历方法的入参数组,通过位置角标获取对应的ValueHolder对象;如果获取不到,则尝试在genericArgumentValues对象中查找;找到的话,会根据是否已经转换(converted为true),来选择对于的属性值;如果converted为true,则选择convertedValue属性对象如果converted为false,则选择value经过TypeConverter类型转换类进行转换得到的对象;
复制代码

3. 入参解析缓存机制

经过 spring 创建的对象,并不会只有一次,所以为了提高第二次的创建对象,设计了缓存机制;

其缓存机制主要是为了减少查找目标构建对象的方法;至于入参是否有必要在解析,是根据 ConstructorArgumentValues 对象的 ValueHolder 对象中的 converted 是否为 false,只要有一个为 false,那就意味着有必要进行解析;

public class RootBeanDefinition {  /**   * 为了避免并发实例同一个对象,需要一个锁来解决并发问题   */  final Object constructorArgumentLock = new Object();  /**   * 存放第一次构建对象的方法   */  Executable resolvedConstructorOrFactoryMethod;  /**   * 第一次解析后,就会设置为true   */  boolean constructorArgumentsResolved = false;  /**   * 存放解析后的入参对象   */  Object[] resolvedConstructorArguments;  /**   * 存放未解析的入参对象   */  Object[] preparedConstructorArguments;  /**   * 只缓存工厂方法,如果构建是通过工厂方法方式,   * 那么该属性与resolvedConstructorOrFactoryMethod是同一个对象   */  volatile Method factoryMethodToIntrospect;}
复制代码

在有入参构建对象的逻辑图基础在添加缓存机制,如下图:

为了处理是否有必要解析入参这个场景,就需要在 ValueHolder、ArgumentsHolder 对象添加额外的属性,来存放解析前的对象;

public class ValueHolder{  /**   * 原始值对象   */  private Object value;  /**   * 是否已经转换过了,也就是意味着是否已经解析过了   */  private boolean converted = false;  /**   * 解析后的对象   */  private Object convertedValue;  /**   * 原始入参对象   */  private Object source;  }public class ArgumentsHolder {  /**   * 原生对象   */  public final Object[] arguments;  /**   * 解析后的入参对象集合   */  public final Object[] rawArguments;  /**   * 未解析的入参对象   */  public final Object[] preparedArguments;  /**   * 只要有一个ValueHolder对象的converted属性为true,resolveNecessary只就会设置为true   */  public boolean resolveNecessary = false;}
复制代码


得到 ArgumentsHolder 后,会将其存放到 RootBeanDefinition 对象的属性中;

4. 应用入参构建对象

上面介绍了有 BeanDefinition 结构中入参,但还有另外的一个场景,那就是由应用程序传过来的入参,这里简称【应用入参】;针对该场景,我们就不需要采取缓存机制了;所以在上面的流程基础上添加【应用入参】的逻辑,如图所示:

5. 创建代理对象

当需要指定方法需要做增强操作时,就需要以代理对象创建的形式;

public abstract class AbstractBeanDefinition {  /**   * 需要覆盖当前类中的方法   */  private MethodOverrides methodOverrides;}
复制代码

这块逻辑的实现,可以查阅《spring 的 IOC 使用以及原理》中有关 Lookup 注解的使用;

2.2 作用域

既然创建出对象了,那么就需要考虑这个对象的所影响区域,也可以理解为对创建对象这个动作进行影响;所以,需要增加一个属性来记录作用域信息;

public abstract class AbstractBeanDefinition {  /**   * 作用域,这里有默认实现的几种区域;   * 1. "" 或 singleton =》 单例   * 2. prototype =》 原型   * 3. 其他作用域   */	private String scope = SCOPE_DEFAULT;}
复制代码

这里补充一下作用域的概念,网上也有,这里就简单的讲一下;

  • 单例: 只有第一次创建对象后,后续就不再创建新的对象,直接复用现有的对象;意味着,在 spring 容器中会有一块内存区域来存放构建好的实例对象;可以简单来说,有缓存机制;

  • 原型:每次创建对象,都是完整的走一遍对象创建流程,并不会缓存起来;

  • 其他:spring 提供了其他类型的作用域,但前提是往 spring 容器中注入 Scope 的实现类才行;

2.3 前期依赖对象

在创建对象过程中,对象中的属性是依赖对象,我这里将其定义为【后期依赖对象】,即创建对象后,再去创建属性对应的对象;然而有特殊的场景,就是创建当前对象前,先创建其他对象,我这里将其定义为【前期依赖对象】;之所以要有【前期依赖对象】,我自己理解为:提前发现问题,减少不必要的初始化动作;例如要创建对象 A,依赖对象的有 B、C 等对象,当创建 C 对象时会发生错误异常;如果先创建了 A 对象,再去创建 C 对象,那么创建 A 对象这个动作是没有必要做的;所以,这个【前期依赖对象】起到了校验效果;

public abstract class AbstractBeanDefinition {  /**   * 里面的元素是对象名称   */	private String[] dependsOn;}
复制代码

三. 对象属性注入

当对象创建后,接下来就会对该对象中的属性进行填充;

3.1 自动注入模式

public abstract class AbstractBeanDefinition {  /**   * 自动注入模式   * 0(AUTOWIRE_NO) => 手动注入模式   * 1(AUTOWIRE_BY_NAME)=》根据属性名自动注入模式   * 2(AUTOWIRE_BY_TYPE)=》根据属性类型自动注入模式   * 3(AUTOWIRE_CONSTRUCTOR)=》根据构造方式自动注入模式   * 4(AUTOWIRE_AUTODETECT)=》自动检测模式,根据目标类的构造函数中是否有入参;   * 	如果没有参数,则使用 AUTOWIRE_BY_TYPE ;否则使用 AUTOWIRE_CONSTRUCTOR    */	private int autowireMode = AUTOWIRE_NO;}
复制代码

在属性注入环节中使用该模式的常见,流程如下:

上面的流程图中,只涉及三种模式,其余的两种模式,会在哪些场景下使用呢?

  • AUTOWIRE_CONSTRUCTOR 在构建对象时使用。让其构建对象时更加倾向通构造方法去创建对象而已;

  • AUTOWIRE_NO 并无在任何场景下使用,也就是说框架对其不做任何处理;

3.2 有属性对象

public abstract class AbstractBeanDefinition {  /**   * 对对象的属性进行注入所存放的对象   */	private MutablePropertyValues propertyValues;}public class MutablePropertyValues{  /**   * 进行   */  private final List<PropertyValue> propertyValueList;  /**   * 已经处理过的属性名   */  private Set<String> processedProperties;  /**   * 是否已经进行转换过   */  private volatile boolean converted = false;}
复制代码

其流程如下:

3.3 Hook 属性注入

在 spring 容器有提供 hook,也就是我们可以通过插件的方式实现属性注入;流程如下:

1. 外部属性管理

从上面的场景来看,不难梳理属性注入的优先级:Hook > PropertiesValues > AutoMode;

一旦有优先使用 PropertiesValues 的方式的场景,我们直接在属性声明设置绕过 Hook 的相关代码,例如 Autowired 注解,我们可以不使用该注解即可;

是否有不去掉 @Autowired 注解,也可以优先 PropertiesValues 属性注入呢?是有的,我们需要一个属性来记录哪些属性不需要通过 Hook 来进行属性注入。

public class RootBeanDefinition {  /**   * 创建对象后,调用前置Hook进行处理,为了避免并发问题,需要加锁   */	final Object postProcessingLock = new Object();  /**   * true表明已经调用前置Hook进行处理过了,无需再次调用;   */  boolean postProcessed = false;  /**   * 由外部程序所管理的属性列表;这里的外部程序主要指的Hook插件   */  private Set<Member> externallyManagedConfigMembers;}
复制代码

只要我们提前将相关的属性保存到【externallyManagedConfigMembers】对象中去,那么意味其他 Hook 插件程序无权对其属性进行操作;只有对应的 Hook 有权使用;

该【externallyManagedConfigMembers】对象的元素注入,是在创建对象后,属性注入前调用处理的;是调用下面的实现类来处理;

public interface MergedBeanDefinitionPostProcessor	void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition,                                        Class<?> beanType, String beanName);}
复制代码

3.4 属性依赖检查策略

当对象的属性注入结束后,需要检测是否有遗漏的属性未注入的;这里有四种策略,来决定属性依赖检测的方式;

public abstract class AbstractBeanDefinition {  /**   * 属性依赖检查策略   * 0(DEPENDENCY_CHECK_NONE)不需要检查   * 1(DEPENDENCY_CHECK_OBJECTS)只检查应用类型   * 2(DEPENDENCY_CHECK_SIMPLE)只检查基础类型   * 3(DEPENDENCY_CHECK_ALL)检查所有   */	private int dependencyCheck = DEPENDENCY_CHECK_NONE;}
复制代码

一般来说,需要一块内存区域记录哪些属性已经初始化的,然后遍历目标类的属性列表,来检查是否有属性未初始化的;然而在 spring 中,并没有这个内存区域来记录这些信息;这块的逻辑,个人理解是有问题的;所幸的是,spring 并没有提供类似注解的形式去修改该属性,所以一般都不会触发属性依赖检测;

四. 对象初始化以及销毁

当对象的属性注入后,将会触发初始化动作;一般来说,实例化动作指的是创建对象;而初始化动作指的调用对象的指定的方法;这里只讲应用程序执行指定的方法,不包含框架自身所提供的方法;

当对象被销毁时,对应的调用对象的销毁方法;

4.1 init&destroy 方法

public abstract class AbstractBeanDefinition {  /**   * 是否强制调用init方法;这一块只是呈现了校验动作;   * 也就是说当其值为true时,init方法必须存在,否则报错   */  private boolean enforceInitMethod = true;  /**   * 与enforceInitMethod的处理逻辑相似   */  private boolean enforceDestroyMethod = true;  /**   * 初始化调用的方法   */	private String initMethodName;  /**   * 销毁对象所调用的方法   */  private String destroyMethodName;  }
复制代码

4.2 Hook 的 init&destroy 方法

这里的 init 方法,是由 Hook 插件自行去调用的;

public class RootBeanDefinition {  /**   * 由外部程序所管理的init方法列表;这里的外部程序主要指的Hook插件   */  private Set<String> externallyManagedInitMethods;   /**   * 由外部程序所管理的销毁方法列表;这里的外部程序主要指的Hook插件   */  private Set<String> externallyManagedDestroyMethods;}
复制代码

这个逻辑,跟 3.3.1 节一样,这里不再阐述;


五. 对象检索

有关具体的属性对象检索逻辑,可以查阅《spring 篇之属性注入》,这里只阐述相关的属性值描述

从这篇文章中得到过滤目标对象的大体过程是:判断是否是候选人名单 > 判断泛型类型是否匹配的 > 判断限定是否匹配的;

1. 候选人名单

public abstract class AbstractBeanDefinition {  /**   * 是否将该实例对象作为候选者;   */  private boolean autowireCandidate = true;}
复制代码

2. 泛型匹配

public class RootBeanDefinition {  /**   * 当做解析后的目标对象的泛型类型   */  volatile ResolvableType targetType;  /**   * 缓存解析后的目标对象的class类型   */  volatile Class<?> resolvedTargetType;  volatile Method factoryMethodToIntrospect;  }
复制代码

有关泛型校验逻辑较为复杂,会以另一篇文章进行介绍,这里不再阐述;

3. 限定匹配

public abstract class AbstractBeanDefinition {  /**   * 限定注解对象集合   */  private final Map<String, AutowireCandidateQualifier> qualifiers;}public class RootBeanDefinition{  /**   * 限定注解   */  private AnnotatedElement qualifiedElement;}
复制代码

上面这两个属性,在常规的应用程序是很少直接使用的;而是直接在类、属性、方法等声明处,表明限定注解;由于没有看过 spring-test 框架源代码,所以只能猜想该 TEST 框架有可能会使用其操作;

4. 主候选人名单

当经过上面层层的过滤,仍然有多个依赖对象时,需要一个策略,来选择最合适的对象;其中优先级最高的就是这个 primary 属性;

public abstract class AbstractBeanDefinition {  /**   * 是否将该实例对象作为主要的候选者;   * 我们应该谨慎使用该属性,一旦出现两个实例对象都是primary的话,程序就会抛出异常;   */  private boolean primary = false;}
复制代码

六. 其他

6.1 懒加载

public abstract class AbstractBeanDefinition {  /**   * 如果为true时,在spring容器启动,就会去创建对象   * 如果为false时,在使用时,才会触发对象创建   */  private boolean lazyInit = false;}
复制代码

6.2 特殊场景

有关 BeanDefinition 结构中大部分的定义都介绍了,只剩下小部分,都是一些特殊特殊场景使用;有后续有使用该场景时,再进行解读;

七. 总结

一个 IOC 框架为围绕对象的构建、属性注入、对象初始化、对象销毁整个环节进行的。上面只是罗列了关键的逻辑,至于一些特殊场景,并没有考虑在内;例如 Hook 机制,其会影响对象创建过程,甚至会改变;

如果结合 Hook 逻辑,那么其就会变得及其复杂,很难解读;

可以查阅我早期发表的文章,在看其文章时,最好是集合源代码阅读;


发布于: 2022 年 08 月 07 日阅读数: 85
用户头像

邱学喆

关注

计算机原理的深度解读,源码分析。 2018.08.26 加入

在IT领域keep Learning。要知其然,也要知其所以然。原理的爱好,源码的阅读。输出我对原理以及源码解读的理解。个人的仓库:https://gitee.com/Michael_Chan

评论

发布
暂无评论
深入了解 Spring篇之BeanDefinition结构_对象初始化_邱学喆_InfoQ写作社区