深入了解 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 逻辑,那么其就会变得及其复杂,很难解读;

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

  • 发表于:
  • 本文为 InfoQ 中文站特供稿件
  • 首发地址https://www.infoq.cn/article/3bc11fabbb45242c94164c2c4
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码关注腾讯云开发者

领取腾讯云代金券