【Spring】容器是怎么启动的呢?快上车带你一探究竟!

Spring容器初始化过程

公众号:完美的工程学

gitte地址:https://gitee.com/duchenxi/total-war

这篇文章主要介绍基于注解的spring容器的启动过程,主要介绍流程上面比较关键的代码,至于流程中的一些细节本文中只会简单地提一下,关于这些细节的展开我会另写文章说明。

先对整体流程做一个总结(思维导图也在我的gitte里面):

image-20210618000855219

首先我们创建一个基于注解的ApplicationContext,如下代码所示:

image-20210613210813073

可以看到我们创建了一个AnnotationConfigApplicationContext这样的一个基于注解的容器,并且传入了一个包路径作为入参,那么实例化一个容器究竟进过了什么样的过程呢?

我们点开AnnotationConfigApplicationContext的构造方法,从上面的注释可以看到这个构造方法作用是构造一个基于注解的容器,并且根据从给定的包路径下扫描组件,注册这些组件的定义(也就是很重要的一个概念BeanDefinition),之后再原子性的刷新容器。从方法中的三行代码可以很清楚的看出大致的逻辑:1.构造一个容器。2.扫描包路径。3.刷新。

接下来我们细细地探究一下这三个过程。

this

我们点开this()方法,忽略掉spring新版本中的步骤相关记录的代码可以看到主要的逻辑只有两行即构造一个reader和一个scanner。

image-20210613211000881

我们点开AnnotatedBeanDefinitionReader的构造方法,可以在方法的注释上面看到这个方法的作用是根据给定的注册表创建一个Reader对象,并且会创建一个标准环境。可以看到这个方法的入参是一个BeanDefinitionRegistry的注册表,而通过上面的分析可以知道传入的实际参数其实就是AnnotationConfigApplicationContext容器本身而AnnotationConfigApplicationContext正是间接地继承了BeanDefinitionRegistry由此可见容器其实就包含了bean注册表的功能

image-20210613211718227

我们跟着这个方法继续往下看,这里调用了另一个reader的构造方法,传入了容器以及一个环境,通过代码getOrCreateEnvironment(registry)可以知道这里根据注册表创建了一个环境,环境可以用来获取一些系统配置,在这里我们就不深究了。

在reader的构造方法里面我们可以看到这样一行代码,字面的意思就是注册注解的后置处理器,没错这个方法会将一些比较重要的注解解析的后置处理器放到容器中。

image-20210615130518364

我们点开这个方法,根据方法上面的注解我们就可以知道这个方法是用来往给定的注册表中添加一些注解的后置处理器的。这里面就注册了@Order、@Lazy、@Configuration、@Bean、@Autowire、@Value、@PostConstruct、@EventListener等等注解,关于后置处理器对这些注解的处理我之后会写额外的文章解析这些细节。image-20210615131440284

关于reader的创建过程到这里就差不多了,接下来是scaner的创建过程,scaner的创建过程和reader的流程差不多但是在这个流程中有一个注册注解过滤器的过程,注册了指定的注解过滤器,容器在进行包扫描的时候才会去解析指定的注解,在这里是添加了一个@Component注解,同时也会隐式地注册相关的注解过滤器包括@Repository、@Service、@Controller、@ManagedBean、@Name等等。

好了,完后了上面的步骤可以认为是整个Spring容器已经初始化完成了,总结一下就是创建了一个容器,容器中包含注册表、bean工厂、环境、注册了重要的注解后置处理器、注册了基本的注解过滤器,但是这个容器还是不完整的,因为现在对于容器来说里面是没有bean的。

获取BeanDefinition

回到容器的构造方法里面继续往下看,接下来是scan方法,入参是我们指定的包路径。根据这个方法的字面意思我们就能知道这个方法是扫描整个包路径下的类并将标注了指定注解的类变成bean放到容器里面。

image-20210615132949683

我们点进这个方法可以看到这里使用了之前在容器创建时候生成的scanner对象的sacn方法来对指定的包路径进行扫描。

image-20210615133255617

我们跳过一些不重要的代码直接查看扫描部分的实现直接看doScan方法,这里面的逻辑主要有三个部分,第一步先从指定的包路径中扫描出所有的指定注解的beanDefinition,第二步再对这些bean定义做一些后置处理包括一些属性的设置,第三步检查这个beanDefinition是否以及存在如果不存在的话就将beanDefinition封装成definitionHolder放到注册表中去。

关于注册表注册definitionHolder的逻辑其实很简单,主要干了两件事情第一个是根据definitionHolder中记录的bean配置的别名将别名和bean的实际名放到aliasMap做一个映射,第二件事就是将beanName和beanDefinition放到beanDefinitionMap中让beanName和定义做一个映射。beanDefinition在Spring中是一个很重要的概念,spring通过类加载来获取类的信息,然后将这些信息封装成一个beanDefinition,之后的bean实例化是根据这个beanDefinition来实例化的,关于这些我之后会在bean声明周期部分详细讲解。

image-20210615143930843

我们先点开findCandidateComponents()方法,看看spring在初始化的时候是怎样将beanDefinition放到容器中的。

在这里首先是获取指定包路径下的所有类的资源,在包路径下并不是所有的类都是要放到bean容器中的所以这里通过两个判断去过滤这些所有的类。

在代码中的第一个判断会对当前检查的类进行判断,还记得之前的注解过滤器吗,在这里就起作用了,这里会判断当前的类是否包含了@Component。当然@Controller、@Service等我们熟知的注解里面是包含@Component注释的。

第二个判断主要是判断当前这个类是不是接口或者是抽象类以及是否被@Lookup修饰,如果是的话就不被放到容器中。

image-20210615151231537

我们点开第一个判断的方法可以看到这里先是判断了一下当前类的注解是否包含被排除的注解,如果有的话就直接返回false也就是不被加入到容器中。下面一个循环则是去校验是否包含了@Component注解。这里的match方法不光检验了当前类是否包含@Component注解而且在当前类不包含@Component注解的情况下还去判断了当前类的父类或者是父接口是否包含了@Component注解,只要符合这样的情况都是可以被加入到容器中的。这里就不展开讲了,有兴趣的同学可以看一下源码。而if判断中的isConditionMatch()方法是为了处理@Conditional注解配置的条件注入。

image-20210615171724646

上面部分的内容就是容器初始化的时候扫描指定的包路径,将该包路径下的需要被放到容器的bean扫描出来的主要流程了。总结一下就是:1.扫描指定的文件资源。2.判断指定的文件资源是否符合条件,将符合提交的beanDefinition返回。3.针对于这些被返回的beanDefinition做后置处理。4.将beanDefinition放到容器的beanDefinitionMap

refresh

接下来是容器初始化过程中的重头戏:刷新容器。这个步骤会对容器进行各种各样的设置,同时这个步骤是被sychronized修饰的,是线程安全的。

总结来说主要有以下几个步骤:

1.准备刷新

2.刷新并获取beanFactroy

2.准备beanFactory

3.添加不同类型的容器特定的bean后置处理器(hook)

4.添加并执行bean工厂的后置处理器

5.添加bean后置处理器

6.国际化处理

7.初始化事件广播

8.执行不同类型容器的特定的处理(hook)

9.注册事件监听器

10.实例化所有的需要被实例化的bean

11.完成初始化

image-20210616101420088

准备刷新

准备刷新步骤主要是完成系统的配置的校验,同时作为整个容器刷新的开始步骤会设置标记量标记容器刷新过程的状态,如下图的closed和active的标记量。同时这里还提供了一个钩子函数(hook)为特定的容器来提供添加特定的配置的机会。之后会验证环境(Environent)中的配置文件。最后会初始化监听器。

image-20210616105504955

刷新并获取beanFactroy

其实这个步骤只是简单地去获取beanFactory,但是在获取容器中的beanFactory之前会先设置一下beanFactory的刷新状态。refreshed是一个AtomicBoolean类型的属性,这里通过cas的方式来尝试修改刷新的状态,如果刷新成功了那么就继续之后的整体流程,如果当前的refreshed是true就表示当前的beanFactory正在被别的线程刷新就会抛出异常:GenericApplicationContext类型的容器不支持多个线程刷新。

image-20210616110621451

准备beanFactory

接下来对获取到的beanFactory进行一些设置,我们点开prepareBeanFactory方法可以看到这个方法里面有两个比较重要的部分,第一个是向beanFactory中添加了一个ApplicationContextAwareProcessor后置处理器,这个处理器可以为bean注入一些容器层面的实例如果bean需要的话。然后接下来调用了很多ignoreDependencyInterface,这个方法是为了告诉在bean实例化的时候如果需要注入这个方法处理过的bean那么就不会去直接生成这个依赖的bean,而是通过上面提到的后置处理器来将这些对象set到要生成的bean的对象中去。

接下来还可以看到beanFactory还调用了redisterResolvableDependency方法,这个方法的作用就是当指定类型类有多个子类实现的时候这里就可以指定去获取哪个bean。

之后就是将一些容器运行的基本的bean注册到beanFactory中。

image-20210616160303301

添加不同类型的容器特定的bean后置处理器(hook)

在准备玩beanFacory之后Spring提供了一个模板方法也就是钩子函数提供给继承AbstractApplicationContext的其他的容器实现,比如WebApplicationContext等等,在当前的类里面是一个空实现,通过阅读方法上的注释我们可以知道这个方法提供了在容器中的beanFactory完成了基础的初始化之后,并且所有bean定义都被加载了但是还没有实例化的时候注册子类需要添加的特殊的bean后置处理器的功能。

image-20210616161302346

添加并执行bean工厂的后置处理器

在完成了上面的步骤之后,spring容器调用了invokeBeanFactoryPostProcessors方法,这个方法主要是触发代码中被注入容器的BeanDefinitionRegistryPostProcessor和BeanFactoryPostProcessor,在触发之后将这些后置处理器放到容器的map中缓存。同时触发以及添加到缓存的后置处理器是有顺序的,先处理有@PriorityOrdered注解修饰的再处理@Ordered修饰的,同时也是按照各自注解中的顺序进行排序处理。

image-20210617200233966

添加bean后置处理器

完成了bean注册后置处理器以及bean工厂后置处理器之后就是对bean创建的后置处理器的添加。spring调用了registerBeanPostProcessors方法,这个方法的处理逻辑和上面步骤中处理方式类似。先注册@PriorityOrdered修改的后置处理器,再处理@Ordered修饰的后置处理器,同时在注册之前会根据注解中的顺序对后置处理器进行排序保证处理器的顺序执行。但是需要注意的是在这里容器还没有对bean进行实例化,所以这些后置处理器在这里不执行只是先注册到容器,等待之后的getBean方法中的流程进行beanPostProcessor的调用。关于getBean方法中复杂的流程的详细讲解之后会在另一片讲解bean生命周期的文章中。

image-20210617201011168

国际化处理

这个不重要。

初始化事件广播

这里的处理方式很简单只是将容器的applicationEventMulticaster赋值,即从容器获取一个beanName是applicationEventMulticaster的一个bean。

image-20210617201800090

关于ApplicationEventMulticaster其实这就是一个提供管理spring中的Listener的管理类。

image-20210617202015986

而且对于Listener的执行是spring中内置的一个线程池来进行处理。

image-20210617202220006

执行不同类型容器的特定的处理(hook)

从方法的注解上面我们就可以知道这个提供了不同的容器自己的独特的对容器的处理机制,默认是一个空实现也就是一个模板方法。

image-20210617202249205

注册事件监听器

这个步骤向容器中注册了所有实现了ApplicationListener接口的事件处理器,同时执行了早期的事件处理器。关于事件处理器Listener的讲解也会在另一篇文章中展开。

image-20210617202617100

实例化所有的需要被实例化的bean

这个步骤在整个refresh中算是一个比较关键的步骤,这一步会将整个容器中不是懒加载的注册了的bean全部实例化。

首先实例化转化器,再实例化Aware。之后再实例化所有剩余的不是懒加载的bean,在这个步骤之前冻结了所有的beanDefinition保证了在实例化的过程中这些bean的定义不会被修改。

image-20210617224621936

我们点进preInstantiateSingletons这个方法,这个方法不长也很容易理解。

首先从之前的缓存中获取所有的注册到缓存的beanName,之后循环创建这些bean。在创建的时候会先合并当前bean和其父类bean的定义也就是合并父类中的属性和方法,之后bean需要满足不是抽象的并且不是懒加载且是单例才会被创建。在创建的时候会先将当前的beanName加上一个工厂bean特有的前缀,再判断这个beanName是不是符合工厂beanName,之后再判断这个工厂bean是不是迫切需要的,如果是的话就调用getBean方法来生产这个工厂bean(注意是实例化工厂bean而不是工厂bean生产的bean镀锡)。如果不是工厂bean的话也是直接实例化这个bean。

完成了这些bean的初始化操作之后就需要执行容器中注册并已经实例化的beanPostProcess来处理这些刚刚被实例化的bean了。首先根据beanName从一级缓存也就是单例缓存中获取刚刚被创建的bean,之后再根据这些单例触发其后置处理器完成bean的整个创建。

image-20210617225826677

在这里我特意打断点看了一下,容器里面执行到这个步骤默认有这么些bean(忽略掉两个my开头的bean是我自己创建的),大部分都是后置处理器以及一些配置bean:

image-20210617231638389

完成初始化

容器进行到这一步已近完成了更新了,现在要做的只是一些收尾性的工作。

首先清除掉一些不需要的缓存节省内存空间,之后调用声明周期的后置处理器,之后发布一个容器已经启动完成的事件,最后将这个容器注册到一个容器的集合中去。

image-20210617232600021

refresh最后

值得一提的是如果上面的过程中抛出了异常也就是容器在初始化的过程中失败了那么容器并不会马上停止工作,而是会销毁掉在抛出异常之前生成的所有的bean。同时也会清除掉在容器启动过程中产生的之后不会使用到的缓存。

image-20210617233039369

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值