一. 概述
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 构建对象
在常规开发中,如果要创建对象,有如下方式:
- 通过{静态}工厂方法进行创建,例如:
publicclassFactory{ public {static} Object<?>createBean(){ //.... } }
复制代码
- 通过构造方法进行创建,例如
publicclassTest { publicstaticvoidmain(String [] args) throwsException { Constructor<Person>constructor=Person.class.getConstructor(); Personman=constructor.newInstance(); System.out.println(man); } publicstaticclassPerson{ privateStringname; publicPerson() { } publicStringtoString() { return"Person{"+"name='"+name+'\''+'}'; } } }
复制代码
- 通过 Supplier 方式进行创建,例如:
Supplier<T>instanceSupplier=newSupplier(){ publicTget(){ //.... } }
复制代码
- 通过 cglib 方式进行创建增强版对象。
除 Supplier 外,工厂方法和构造方法都是通过反射调用 Method 对象进行对象创建。所以 spring 提供一个接口 InstantiationStrategy,其将调用 Method 对象进行对象创建的逻辑封装到 SimpleInstantiationStrategy 实现类里面;
那么 BeanDefinition 结构中就包含了上面的几个创建对象的内容;
publicabstractclassAbstractBeanDefinition { privatevolatileObjectbeanClass; /*** Supplier方式*/privateSupplier<?>instanceSupplier; /*** 工厂方法进行创建*/privateStringfactoryBeanName; privateStringfactoryMethodName; }
复制代码
1. 无参数构建对象
既然提供如上方式进行对象创建,那么意味着会有优先级;那么我们就可以设计一个简单的流程(无参数的方法),对目标类型进行实例化对象,如图所示:
2. 有入参构建对象
在无参构建对象过程基础上,添加这么一个逻辑,【通过指定规则从中选出目标方法】,如下图;
指定规则如下:
匹配方法入参类型与入参对象类型的逻辑。如果都不匹配,意味着找不到合适的方法,直接报错;如果有匹配的方法有2个以上,那我们就需要决策了,具体需要哪种策略了;目前有两种策略,如下: 1. 宽松策略:优先选择第一个方法; 2. 严格策略:再次精细化类型匹配,根据类型的继承层级来判断,层级越低,说明类型关系越接近,则优先选择关系越接近的方法; ps. 具体的实现在ArgumentsHolder类
复制代码
具体采用哪种策略,直接存放到 BeanDefinition 中;
publicabstractclassAbstractBeanDefinition { /*** true代表是采用严格策略;* false代表是采用宽松策略*/privatebooleanlenientConstructorResolution=true; }
复制代码
上面的过程中,通过反射来获取的方法列表,这里有两个可能性,是只获取 public 方法,还是获取所有的方法;
publicabstractclassAbstractBeanDefinition { /*** true代表获取所有方法;* false代表是只获取public方法*/privatebooleannonPublicAccessAllowed=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,那就意味着有必要进行解析;
publicclassRootBeanDefinition { /*** 为了避免并发实例同一个对象,需要一个锁来解决并发问题*/finalObjectconstructorArgumentLock=newObject(); /*** 存放第一次构建对象的方法*/ExecutableresolvedConstructorOrFactoryMethod; /*** 第一次解析后,就会设置为true*/booleanconstructorArgumentsResolved=false; /*** 存放解析后的入参对象*/Object[] resolvedConstructorArguments; /*** 存放未解析的入参对象*/Object[] preparedConstructorArguments; /*** 只缓存工厂方法,如果构建是通过工厂方法方式,* 那么该属性与resolvedConstructorOrFactoryMethod是同一个对象*/volatileMethodfactoryMethodToIntrospect; }
复制代码
在有入参构建对象的逻辑图基础在添加缓存机制,如下图:
为了处理是否有必要解析入参这个场景,就需要在 ValueHolder、ArgumentsHolder 对象添加额外的属性,来存放解析前的对象;
publicclassValueHolder{ /*** 原始值对象*/privateObjectvalue; /*** 是否已经转换过了,也就是意味着是否已经解析过了*/privatebooleanconverted=false; /*** 解析后的对象*/privateObjectconvertedValue; /*** 原始入参对象*/privateObjectsource; } publicclassArgumentsHolder { /*** 原生对象*/publicfinalObject[] arguments; /*** 解析后的入参对象集合*/publicfinalObject[] rawArguments; /*** 未解析的入参对象*/publicfinalObject[] preparedArguments; /*** 只要有一个ValueHolder对象的converted属性为true,resolveNecessary只就会设置为true*/publicbooleanresolveNecessary=false; }
复制代码
得到 ArgumentsHolder 后,会将其存放到 RootBeanDefinition 对象的属性中;
4. 应用入参构建对象
上面介绍了有 BeanDefinition 结构中入参,但还有另外的一个场景,那就是由应用程序传过来的入参,这里简称【应用入参】;针对该场景,我们就不需要采取缓存机制了;所以在上面的流程基础上添加【应用入参】的逻辑,如图所示:
5. 创建代理对象
当需要指定方法需要做增强操作时,就需要以代理对象创建的形式;
publicabstractclassAbstractBeanDefinition { /*** 需要覆盖当前类中的方法*/privateMethodOverridesmethodOverrides; }
复制代码
这块逻辑的实现,可以查阅《spring 的 IOC 使用以及原理》中有关 Lookup 注解的使用;
2.2 作用域
既然创建出对象了,那么就需要考虑这个对象的所影响区域,也可以理解为对创建对象这个动作进行影响;所以,需要增加一个属性来记录作用域信息;
publicabstractclassAbstractBeanDefinition { /*** 作用域,这里有默认实现的几种区域;* 1. "" 或 singleton =》 单例* 2. prototype =》 原型* 3. 其他作用域*/privateStringscope=SCOPE_DEFAULT; }
复制代码
这里补充一下作用域的概念,网上也有,这里就简单的讲一下;
- 单例: 只有第一次创建对象后,后续就不再创建新的对象,直接复用现有的对象;意味着,在 spring 容器中会有一块内存区域来存放构建好的实例对象;可以简单来说,有缓存机制;
- 原型:每次创建对象,都是完整的走一遍对象创建流程,并不会缓存起来;
- 其他:spring 提供了其他类型的作用域,但前提是往 spring 容器中注入 Scope 的实现类才行;
2.3 前期依赖对象
在创建对象过程中,对象中的属性是依赖对象,我这里将其定义为【后期依赖对象】,即创建对象后,再去创建属性对应的对象;然而有特殊的场景,就是创建当前对象前,先创建其他对象,我这里将其定义为【前期依赖对象】;之所以要有【前期依赖对象】,我自己理解为:提前发现问题,减少不必要的初始化动作;例如要创建对象 A,依赖对象的有 B、C 等对象,当创建 C 对象时会发生错误异常;如果先创建了 A 对象,再去创建 C 对象,那么创建 A 对象这个动作是没有必要做的;所以,这个【前期依赖对象】起到了校验效果;
publicabstractclassAbstractBeanDefinition { /*** 里面的元素是对象名称*/privateString[] dependsOn; }
复制代码
三. 对象属性注入
当对象创建后,接下来就会对该对象中的属性进行填充;
3.1 自动注入模式
publicabstractclassAbstractBeanDefinition { /*** 自动注入模式* 0(AUTOWIRE_NO) => 手动注入模式* 1(AUTOWIRE_BY_NAME)=》根据属性名自动注入模式* 2(AUTOWIRE_BY_TYPE)=》根据属性类型自动注入模式* 3(AUTOWIRE_CONSTRUCTOR)=》根据构造方式自动注入模式* 4(AUTOWIRE_AUTODETECT)=》自动检测模式,根据目标类的构造函数中是否有入参;* 如果没有参数,则使用 AUTOWIRE_BY_TYPE ;否则使用 AUTOWIRE_CONSTRUCTOR */privateintautowireMode=AUTOWIRE_NO; }
复制代码
在属性注入环节中使用该模式的常见,流程如下:
上面的流程图中,只涉及三种模式,其余的两种模式,会在哪些场景下使用呢?
- AUTOWIRE_CONSTRUCTOR 在构建对象时使用。让其构建对象时更加倾向通构造方法去创建对象而已;
- AUTOWIRE_NO 并无在任何场景下使用,也就是说框架对其不做任何处理;
3.2 有属性对象
publicabstractclassAbstractBeanDefinition { /*** 对对象的属性进行注入所存放的对象*/privateMutablePropertyValuespropertyValues; } publicclassMutablePropertyValues{ /*** 进行*/privatefinalList<PropertyValue>propertyValueList; /*** 已经处理过的属性名*/privateSet<String>processedProperties; /*** 是否已经进行转换过*/privatevolatilebooleanconverted=false; }
复制代码
其流程如下:
3.3 Hook 属性注入
在 spring 容器有提供 hook,也就是我们可以通过插件的方式实现属性注入;流程如下:
1. 外部属性管理
从上面的场景来看,不难梳理属性注入的优先级:Hook > PropertiesValues > AutoMode;
一旦有优先使用 PropertiesValues 的方式的场景,我们直接在属性声明设置绕过 Hook 的相关代码,例如 Autowired 注解,我们可以不使用该注解即可;
是否有不去掉 @Autowired 注解,也可以优先 PropertiesValues 属性注入呢?是有的,我们需要一个属性来记录哪些属性不需要通过 Hook 来进行属性注入。
publicclassRootBeanDefinition { /*** 创建对象后,调用前置Hook进行处理,为了避免并发问题,需要加锁*/finalObjectpostProcessingLock=newObject(); /*** true表明已经调用前置Hook进行处理过了,无需再次调用;*/booleanpostProcessed=false; /*** 由外部程序所管理的属性列表;这里的外部程序主要指的Hook插件*/privateSet<Member>externallyManagedConfigMembers; }
复制代码
只要我们提前将相关的属性保存到【externallyManagedConfigMembers】对象中去,那么意味其他 Hook 插件程序无权对其属性进行操作;只有对应的 Hook 有权使用;
该【externallyManagedConfigMembers】对象的元素注入,是在创建对象后,属性注入前调用处理的;是调用下面的实现类来处理;
publicinterfaceMergedBeanDefinitionPostProcessorvoidpostProcessMergedBeanDefinition(RootBeanDefinitionbeanDefinition, Class<?>beanType, StringbeanName); }
复制代码
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 方法
publicabstractclassAbstractBeanDefinition { /*** 是否强制调用init方法;这一块只是呈现了校验动作;* 也就是说当其值为true时,init方法必须存在,否则报错*/privatebooleanenforceInitMethod=true; /*** 与enforceInitMethod的处理逻辑相似*/privatebooleanenforceDestroyMethod=true; /*** 初始化调用的方法*/privateStringinitMethodName; /*** 销毁对象所调用的方法*/privateStringdestroyMethodName; }
复制代码
4.2 Hook 的 init&destroy 方法
这里的 init 方法,是由 Hook 插件自行去调用的;
publicclassRootBeanDefinition { /*** 由外部程序所管理的init方法列表;这里的外部程序主要指的Hook插件*/privateSet<String>externallyManagedInitMethods; /*** 由外部程序所管理的销毁方法列表;这里的外部程序主要指的Hook插件*/privateSet<String>externallyManagedDestroyMethods; }
复制代码
这个逻辑,跟 3.3.1 节一样,这里不再阐述;
五. 对象检索
有关具体的属性对象检索逻辑,可以查阅《spring 篇之属性注入》,这里只阐述相关的属性值描述
从这篇文章中得到过滤目标对象的大体过程是:判断是否是候选人名单 > 判断泛型类型是否匹配的 > 判断限定是否匹配的;
1. 候选人名单
publicabstractclassAbstractBeanDefinition { /*** 是否将该实例对象作为候选者;*/privatebooleanautowireCandidate=true; }
复制代码
2. 泛型匹配
publicclassRootBeanDefinition { /*** 当做解析后的目标对象的泛型类型*/volatileResolvableTypetargetType; /*** 缓存解析后的目标对象的class类型*/volatileClass<?>resolvedTargetType; volatileMethodfactoryMethodToIntrospect; }
复制代码
有关泛型校验逻辑较为复杂,会以另一篇文章进行介绍,这里不再阐述;
3. 限定匹配
publicabstractclassAbstractBeanDefinition { /** * 限定注解对象集合 */privatefinalMap<String, AutowireCandidateQualifier>qualifiers; } publicclassRootBeanDefinition{ /** * 限定注解 */privateAnnotatedElementqualifiedElement; }
复制代码
上面这两个属性,在常规的应用程序是很少直接使用的;而是直接在类、属性、方法等声明处,表明限定注解;由于没有看过 spring-test 框架源代码,所以只能猜想该 TEST 框架有可能会使用其操作;
4. 主候选人名单
当经过上面层层的过滤,仍然有多个依赖对象时,需要一个策略,来选择最合适的对象;其中优先级最高的就是这个 primary 属性;
publicabstractclassAbstractBeanDefinition { /** * 是否将该实例对象作为主要的候选者; * 我们应该谨慎使用该属性,一旦出现两个实例对象都是primary的话,程序就会抛出异常; */privatebooleanprimary=false; }
复制代码
六. 其他
6.1 懒加载
publicabstractclassAbstractBeanDefinition { /** * 如果为true时,在spring容器启动,就会去创建对象 * 如果为false时,在使用时,才会触发对象创建 */privatebooleanlazyInit=false; }
复制代码
6.2 特殊场景
有关 BeanDefinition 结构中大部分的定义都介绍了,只剩下小部分,都是一些特殊特殊场景使用;有后续有使用该场景时,再进行解读;
七. 总结
一个 IOC 框架为围绕对象的构建、属性注入、对象初始化、对象销毁整个环节进行的。上面只是罗列了关键的逻辑,至于一些特殊场景,并没有考虑在内;例如 Hook 机制,其会影响对象创建过程,甚至会改变;
如果结合 Hook 逻辑,那么其就会变得及其复杂,很难解读;
可以查阅我早期发表的文章,在看其文章时,最好是集合源代码阅读;