一、前言
道家有云:“道生一,一生二,二生三,三生万物。”这句话简单理解就是,世间万物皆是由道衍生出来的,而道则是对万物的一种极致抽象。
在不断深入学习和使用Spring框架的过程中,愈发觉得在Spring框架的设计理念中随处可见前面提到的道家理念。在之前的学习中我们有提到Spring框架中最核心的两个部分,一个是IoC容器,一个是AOP模块,其余功能都是基于这两个核心功能构建起来的。而在IoC容器和AOP模块这两个核心功能中,后者又是基于前者来实现相应的功能。至此,我们不难得出一个不是那么准确的结论,即Spring所提供的全部功能都是通过IoC容器生成的各式对象来实现的。那么,这些各式各样的对象又是如何生成的呢?
遍观网络上大部分Spring系列或者关于Spring IoC容器的博客在谈及Spring Bean生成过程时都喜欢围绕Bean的生命周期进行细致分析,但在Bean成员变量是如何确定、Bean中为什么会包含某些注解等一些Bean定义来源问题以及相应处理流程的讲解上经常是轻描淡写一笔带过(当然笔者之前学习的时候也喜欢干这种事情,哈哈哈~),导致对于Spring中“万物”生成的流程总是感觉理解得不够透彻,好像缺少了什么。其实再往前深挖一步我们就可以得到答案,上面博客中关注的Bean生命周期的分析更多的是针对Bean成员变量设置以及方法代理等流程的分析,而对于Bean定义的设计以及生成的流程却鲜少进行分析讨论。为了更好地理解Spring容器创建对象的过程,我们需要追本溯源,去探索一下Bean对象的源头BeanDefinition的运作原理。
二、为什么需要BeanDefinition?
让我们先抛开IoC容器,抛开Spring,先回归最基本的对象生成流程(需要注意,这里我们谈论的对象生成更多的是属于编码使用层面,而非JVM层面)。从以往学习的经验来看,对象的生成可以分为两步:实例创建和属性赋值。前者创建了一个空的对象,后者则是对这个空对象的成员变量进行赋值处理。如果让我们使用人工的方式来处理这两步其实非常简单,先通过new关键字来创建目标类的实例,然后根据我们的实际需要对目标类实例中的成员变量进行逐一赋值。这里可以通过下面的例子简单感受一下:
// 创建实例
Person person = new Person();
// 进行对象成员变量赋值
person.setName("test");
person.setGender("male");
但是当我们尝试使用代码方式 (即编程式) 来对上述两个步骤进行逻辑抽象时就会发现这其中存在许多难点。
首先,我们遇到的第一个也是最重要的一个问题:应该创建哪个类型的实例? 之所以会遇到这样一个问题主要是Java面向对象语言中的多态特性导致的,即父类引用可以指向子类实现。基于这一特性,我们在进行对象实例化时需要明确指定使用父类或者子类进行实例化,其中如果父类是抽象类或者接口则必须使用指定子类来进行对象的实例化。这就意味着我们需要在对该关联关系进行存储以便在对象实例化时能够按照对应关系进行实例化。
第二个问题:在每次使用该对象时是否需要创建新的对象? 即是否需要使用单例模式?在我们实际开发过程中一般会有两种类型的对象,一种是只会存在于一次请求处理过程中的短周期对象(即每次使用都需要创建新的对象),一种是每次请求使用的都是同一个实例的全局单例对象。前者一般用于进行数据的临时存储,后者一般用于提供通用的数据处理能力。同样,在使用编程式方式来创建对象时我们也需要对该对象创建方式进行存储。
第三个问题:如何使用类提供的构造函数进行对象的创建? 其实这个问题的难点是在于如何使用有参构造函数来完成对象实例的创建。当我们使用无参构造函数时,可以很简单地创建实例而不需要考虑其他问题。但当我们准备使用有参构造函数时,就需要考虑如何获取函数参数列表中每个参数的对象实例,即需要如何设置相应参数哪种类型的对象实例。分析到这里可以发现,我们又绕回到了第一个问题。和第一个问题一样,要想正确地使用构造函数完成预期的实例创建,我们就需要记录构造函数的参数列表和对应参数需要设置的实例对象。
由第三个问题可以引申出第四个问题:如果目标类型只提供了无参构造函数,我们需要如何设置对象实例的成员变量? 这个问题可以说是第三个问题的变种,只不过将成员变量设置的时机由构造函的调用阶段延后至使用对象setter方法来设置成员变量阶段。
除了上面的问题,还有其他一些需要考虑的问题,这里笔者就不一一列举说明了。但是通过上面的分析我们不难得出一个结论:要想通过编程式的方式来自动化地完成指定对象的实例化和赋值处理,我们需要抽象出一个能够描述/存储对象实例内部信息的类型。
分析完最基本的对象生成流程,让我们重新回到Spring对Bean的管理功能中。此时我们会发现,其实Spring的Bean管理就是通过编程式的方法来统一管理Bean的生成和使用,在这一过程中对象的创建和赋值流程完全由框架本身控制而非由开发人员自主控制(即我们常说的控制反转)。就如同我们上面分析的一样,为了实现这一能力,Spring设计并使用BeanDefinition来进行目标对象实例信息的描述。基于BeanDefinition提供的对象实例信息,并结合Java提供的反射机制,Spring实现了道生一以至衍生万物的功能。
注意:下文中的源码均是基于SpringBoot 2.6.10版本进行分析
三、如何生成BeanDefinition?
在上一小节中我们通过分析使用编程式方法创建对象存在的问题去探索了一下BeanDefinition的设计意义,但要想真正使用BeanDefinition,我们还需要了解一下它的生成流程。
1. 普通开发者的BeanDefinition生成流程设计
同样的,让我们先抛开Spring源码本身,以一个开发者的身份思考一下如果要实现生成BeanDefinition这样一个需求需要完成哪些功能。根据上面的分析和我们日常开发的使用经验不难发现,要想生成BeanDefinition需要完成以下两个功能:
- 确定项目中哪些类需要生成BeanDefinition
- 基于目标类对BeanDefinition进行赋值处理
我们先来看一下第一个功能:确定项目中需要生成BeanDefinition的类。从我们日常的开发中可以发现并不是所有的类都需要通过Spring来进行创建和管理,比如DO、VO这些用于数据传输的类,再比如一些提供静态方法的工具类,这些类要么是在需要使用的地方由开发人员主动地创建并使用,要么就是不需要实例化直接使用当前类的静态方法。那么我们应该如何确定需要生成的BeanDefinition的类呢?这里我们需要将其拆分成两步完成,第一步,找到项目中所有由开发人员自定义的类,第二步,根据标识来区分这些自定义的类哪些是需要生成BeanDefinition的。前者可以通过包名扫描出所有属于这个项目的类型,后者可以通过特殊的注解来对相应的类型进行标识。
让我们接着来看一下第二个功能:基于目标类对BeanDefinition进行赋值处理。从上文的分析中我们可以知道BeanDefinition当中存储的都是用于去生成对象实例的类型信息,而要想在运行时去获取一个类型的相关信息相信大部分人的第一反应基本就是通过反射机制来实现 (为什么要说大部分人,这是因为Spring的开发人员并没有采取这一方案,在下面的源码分析阶段我们会具体了解一下其中的原因) 。通过反射机制我们能够获取到当前对象的类型信息、当前对象的注解、依赖的成员变量以及成员变量的注解等等大部分需要的类型信息,然后我们就可以将这些需要的类型信息赋值到BeanDefinition当中。
以上是我们作为一个开发人员基于生成BeanDefinition这样一个需求做出的技术方案揣测,下面让我们结合源码来具体看一下Spring是如何设计生成BeanDefinition的技术方案。
2. Spring开发者的BeanDefinition生成流程设计
从SpringBoot项目的启动类开始向内逐层debug,我们会找到自定义类型的BeanDefinition生成入口org.springframework.context.annotation.ClassPathBeanDefinitionScanner#doScan
(需要注意,这里特别标注了这里的入口是非Spring框架、非二方包内的类型的BeanDefinition生成入口) 。具体可以参考下图:
首先我们来看下方法参数列表中的basePackages。在逐层debug的过程中我们可以看到方法入参中basePackages会通过两种方式获取到,一种是缺省情况下会使用启动类所在的包名作为basePackage,另一种则是从@ComponentScan
注解的设置当中获取。需要注意的是,如果准备使用第二种方式来自定义设置basePackages,在我们实际使用SpringBoot时并不是直接使用@componentScan
来进行basePackages设置的,而是通过@SpringBootApplication
注解的scanBasePackages或scanBasePackageClasses参数来进行设置的。
在了解到basePackages的由来之后我们再来看下上面的方法,可以看到上面的方法实际上主要做了两件事:
- 根据basePackages获取指定自定义类的BeanDefinition实例;
- 基于生成BeanDefinition集合进行额外的包装,并将这些BeanDefinition注册到上下文当中;
这里我们主要探究一下第一部分内容的实现,从上面的代码中可以看到该部分内容的逻辑都放置在public Set<BeanDefinition> findCandidateComponents(String basePackage)
方法当中。通过实际的debug会发现流程最终会进入到该方法内部的scanCandidateComponents(basePackage)
中。该方法的具体内容如下:
从上面的代码中可以看出,Spring的开发者们确实和我们之前猜测的一样,基于basePackages来扫描出所有有可能需要生成BeanDefinition的类文件。但是和我们猜测的不同,这里扫描的不是单纯的类全限定名,而是class文件的文件路径,同时最终生成类型的元数据信息时并没有使用反射机制,而是使用了读取.class文件字节码的方式(这部分逻辑在org.springframework.asm.ClassReader#accept(org.springframework.asm.ClassVisitor, int)
方法中处理,这里不展示具体的代码内容)。相比一些同学会产生疑惑,为什么没有使用简单好用的反射机制,而是使用了极其复杂难用的读取字节码的方式来获取类型的元数据信息?这主要是由于JVM本身特性导致的,在实际的运行过程中,JVM并不会时时刻刻加载全部的类型信息,只会将部分需要使用到的类型加载到虚拟机内存当中。这就意味着在项目启动的初始化过程中,JVM当中可能并没有将对应类型加载到内存当中,使用反射机制并不能获取到对应类型的元数据信息。同时,从效率上来讲通过字节码的方式比使用反射机制会更高。
除此以外,在实际生成BeanDefinition时,Spring框架做了进一步的判断和拦截。和我们上面的分析相同,基于basePackages的.class文件扫描会将所有自定义类型都扫描出来,这其中就包含不少不需要生成BeanDefinition的类型。从上面的代码中我们可以看到,实际的判断和拦截是通过isCandidateComponent(metadataReader)
和isCandidateComponent(sbd)
来完成的。第一个方法中通过基于注解的过滤器(excludeFilter)和包含器(includeFilter)来判断当前类型是否需要生成BeanDefinition,第二个方法在第一个方法的判断基础上进一步针对BeanDefinition进行了判断,如果当前类型属于接口、抽象类或者封闭类(enclosing class) 则舍弃当前BeanDefinition。补充第二判断的主要原因是,开发者在实际开发时可能会将注解标注在不能生成对象实例的类(比如上面提到的三种类型)上,即单纯靠注解进行判断是不完全可信的。
在isCandidateComponent(metadataReader)方法中的包含器会基于@Component注解来判断是否包含当前类型的元数据,根据这一条件,使用包含@Component注解的注解对类进行标记时也会被包含器包含(比如@Configuration、@Service、@Controller、@RestController等)
以上即为根据basePackages获取指定自定义类的BeanDefinition实例的实现过程,在这一过程中Spring还完成了类型元数据相关信息的赋值处理。而在第二部分中更多是基于Spring自身设计以及其他功能的额外信息设置(包含scope、init方法、destory方法等信息的设置),有兴趣的同学可以自行进行源码的阅读,这里笔者就不过多分析了。
四、BeanDefinition的使用
铺垫了这么久,我们终于来到了BeanDefinition使用环节的分析。在Spring框架当中,对于BeanDefinition的使用主要分为两个方面:
- 基于BeanDefinition进行Bean对象的创建;
- 基于BeanDefinition对Bean对象进行额外的加工处理(诸如Bean属性设置、使用BeanPostProcessor对Bean进行额外处理等);
在本篇文章中我们会将目光聚焦在第一个方面,对于第二个方面网上已经有太多的博客进行过分析,这里就不加赘述了(其实我们经常看到的对于Bean生命周期的分析其实就是这里所说的第二个方面的一部分)。
从第一小结的分析当中我们可以看到如果不使用new关键字的方式来进行对象实例的创建,我们就需要使用反射机制来完成。使用反射机制的目的是为了获取到目标类型的构造器Constructor<T> ctor
,然后通过构造器方法的Constructor#newInstance
来创建对应的实例。在Spring框架中就是使用了反射机制中的相应流程来完成对于Bean实例的创建,具体方法org.springframework.beans.BeanUtils#instantiateClass(java.lang.reflect.Constructor<T>, java.lang.Object...)
如下:
这个方法的内部逻辑非常简单,我们需要关注的是方法的参数列表,或者说是该方法参数列表中的值是从什么地方获取到的。为了解决这个问题,我们需要向上回溯,回到org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#createBeanInstance
方法:
从上面展示的代码中我们可以归纳出三种利用BeanDefinition生成对象实例的情况:
- 使用开发自定义的创建实例的方法,即代码中展示的
obtainFromSupplier(instanceSupplier, beanName)
和instantiateUsingFactoryMethod(beanName, mbd, args)
,前者由开发自定义所需的Supplier<?>
,后者则需要开发实现对应的工厂方法; - 使用类默认的无参构造函数进行对象实例的创建,即代码中的
instantiateBean(beanName, mbd)
方法; - 使用特定的构造函数进行对象实例的创建,即代码中的
autowireConstructor(beanName, mbd, ctors, args)
方法;
第一种情况是通过开发者自行控制,是Spring提供一个扩展点,这里不需要做过多关注。第二种方式和第三种方式从本质上来说其实是一种方法,即使用反射机制中的构造器来进行对象实例创建,两者的区别在于一个使用的是默认的无参构造函数,另一个则是通过读取BeanDefinition中指定的首选构造器或候选构造器来完成对象实例的生成。
这里需要补充一点,在
AbstractAutowireCapableBeanFactory#createBeanInstance
方法的第一行Class<?> beanClass = resolveBeanClass(mbd, beanName);
中可以看到,这里通过BeanDefinition进行了目标类型的加载处理,即保证目标类型已经加载到JVM当中,这也解释了为什么后续方法可以使用反射机制来生成对象实例。
在这里我们可以解决BeanUtils#instantiateClass
参数列表中第一个参数Constructor<T>
的来源问题,但是貌似并没有找到解决第二个参数构造函数的参数列表的来源。这里笔者使用了貌似是因为如果我们继续向上回溯会发现上游在最开始设置构造函数的参数args时设置的就是null,也就是说args的值需要向下探索。这里我们需要从autowireConstructor(beanName, mbd, ctors, args)
这个入口向下求解,直到进入org.springframework.beans.factory.support.ConstructorResolver#autowireConstructor
方法内部我们会发现一直找寻的args值出现了:
至此,BeanDefinition在创建对象实例中的使用基本已经分析完毕。
五、总结
不得不说,在阅读分析完BeanDefinition生成和使用的源码设计之后,以往对于Spring框架中Bean管理模块的功能理解才算真正意义上做到了一个闭环。当然这里的闭环并不是非常完美的闭环,因为这里我们只是简单的回答了Bean管理模块中关于Bean创建流程的相关问题,但是对于Bean管理模块是如何支撑Spring框架中诸如事务管理、MVC等其他模块的问题并没有做出相应的回答。这部分内容有兴趣的朋友可以先自行探索,也可以耐心等待笔者后续不知哪一天的更新(要学要写的东西太多,哈哈哈)。
本文在分析和讲解过程中大多只是展示了入口或者关键节点的代码,对于更细节的源码并没有贴出来,所以在学习的过程中最好能自己在结合源码进行debug来实际感受一下。
最后,世事无常,生活不易,愿诸位身体健康,早日升职加薪~~