• Springboot启动流程核心知识点(一):Spring自动装配原理


    目录

    1 项目工程类信息注册

    1.1 @Component形式

    1.2 @ComponentScan形式

    1.3 @Import形式

    1.4 @ImportResource形式

    1.5 beanMethods方式

    1.6 解析接口中的beanMethods方式

    1.7 递归解析当前类的父类

    2 Spring自动装配原理

    3 小结


    对Springboot有所了解的读者,对一些理念想必很熟悉,如Springboot工程会默认加载当前主启动类下的所有类文件到IOC容器,以及Springboot可以通过自动装配的功能,实现对外部功能的无缝引入。

    怎么说呢?大的方向是这样,但是如果更近一步,要问一下这些功能具体是如何实现的,只有通过源码才能完整而清晰的了解其每一步的实现意图。

    下面我们结合Springboot的启动过程,来直接进入源码,再现Springboot的依托主启动类扫包和自动装配原理。

    首先,通过主启动类的main方法进入SpringApplication的run方法:

    1. public static void main(String[] args) {
    2. SpringApplication.run(new Class[]{BookstoreApplication.class}, args);
    3. }

    之后再通过如下顺序:

    this.refreshContext(context); (SpringApplication) --->>>
    
    this.refresh(context); (SpringApplication) --->>>
    
    refresh();(AbstractApplicationContext) --->>>
    
    this.invokeBeanFactoryPostProcessors(beanFactory);(AbstractApplicationContext)

    最终进入类PostProcessorRegistrationDelegate中的BeanFactory后置处理器执行方法invokeBeanFactoryPostProcessors。

    在此方法中,第一次执行方法

    invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry);

    时,会执行一个特殊的BeanDefinitionRegistry后置处理器——ConfigurationClassPostProcessor的postProcessBeanDefinitionRegistry方法。

    在此方法中会通过

    this.processConfigBeanDefinitions(registry);

    下的

    parser.parse(candidates);

    真正进入主启动类下项目工程类信息注册,以及Spring自动装配功能。

    说了这么多,其实是为了让读者可以通过断点,启动任意一个Springboot项目,自己去断点调试一下,验证笔者所说,毕竟眼见为实,耳听为虚,实践才是最好的老师。

    有兴趣的读者,可以直接进入ConfigurationClassPostProcessor类的processConfigBeanDefinitions方法,再进入parser.parse(candidates),就可以直接而快速的了解接下来笔者要讲解的源码。

    1 项目工程类信息注册

    关于bean信息注册的方法,基本都在ConfigurationClassParser这个类中,processConfigBeanDefinitions实现bean信息注册的入口方法在ConfigurationClassParser的parse方法中:

    1. public void parse(Set configCandidates) {
    2. //主启动类bean信息集合
    3. Iterator var2 = configCandidates.iterator();
    4. while(var2.hasNext()) {
    5. BeanDefinitionHolder holder = (BeanDefinitionHolder)var2.next();
    6. BeanDefinition bd = holder.getBeanDefinition();
    7. try {
    8. if (bd instanceof AnnotatedBeanDefinition) {
    9. //注册项目工程类信息
    10. this.parse(((AnnotatedBeanDefinition)bd).getMetadata(), holder.getBeanName());
    11. .......
    12. }
    13. //注册Spring提供默认类信息
    14. this.deferredImportSelectorHandler.process();
    15. }

    this.parse方法主要就是依据其传入的参数,也就是主启动类的bean定义信息BeanDefinition和BeanDefinitionHolder构造一个新的对象ConfigurationClass,再以ConfigurationClass和DEFAULT_EXCLUSION_FILTER,作为参数进入真正的执行方法processConfigurationClass。

    1. private static final Predicate DEFAULT_EXCLUSION_FILTER = (className) -> {
    2. return className.startsWith("java.lang.annotation.") || className.startsWith("org.springframework.stereotype.");
    3. };

    进入processConfigurationClass后,首先会判断当前当前主启动类ConfigurationClass是否需要被拦截,主要是根据@Conditional注解和DEFAULT_EXCLUSION_FILTER的信息来判断:

    1. protected void processConfigurationClass(ConfigurationClass configClass, Predicate filter) throws IOException {
    2. if (!this.conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.PARSE_CONFIGURATION)) {

    在执行完过滤条件后,再以ConfigurationClass和DEFAULT_EXCLUSION_FILTER参数构造一个参数SourceClass,其是ConfigurationClassParser的一个内部类,最终把ConfigurationClass、SourceClass和DEFAULT_EXCLUSION_FILTER三个作为参数,来执行下一步的类信息解析:

    1. do {
    2. sourceClass = this.doProcessConfigurationClass(configClass, sourceClass, filter);
    3. } while(sourceClass != null);

     有人看到这一步就会很奇怪,configClass和sourceClass,这两参数基本都是一样,只不过另一个又包装了一层,这样构造的意义在哪里呢?其原因主要也是为了框架的需要,这样可以利用递归,把主启动类包下的每一个已经加入IOC容器的类都当作是一个配置类,可以任意向下扩展。

    它可以在自身已经被@Component或其他注解形式修饰,在已经加入IOC容器的前提下,通过@ComponentScan注解、@Import注解或者@ImportResource注解,来给IOC容器新增我们需要的指定类。

    下面通过doProcessConfigurationClass方法分别介绍:

    1. protected final ConfigurationClassParser.SourceClass doProcessConfigurationClass(ConfigurationClass configClass, ConfigurationClassParser.SourceClass sourceClass, Predicate filter) throws IOException {
    2. if (configClass.getMetadata().isAnnotated(Component.class.getName())) {
    3. this.processMemberClasses(configClass, sourceClass, filter);
    4. }

    在介绍这个方法之前,我们先说一个前提,只要满足条件的基础上,方法正常执行,会返回一个configClass,被缓存到当前类的一个Map属性configurationClasses中,后面会被构造成bean的定义信息被注册到IOC容器。

    在此基础上,我们会对当前类,有可能出现的扩展情况,进行处理,通俗说,就是在当前类配置了一些属性,需要把新的类信息注入IOC容器

    1.1 @Component形式

    如果当前类带有@Component注解,我们会对其内部类进行解析,看其内部类是否配置了@Component注解,需要被注入IOC容器:

    1. private void processMemberClasses(ConfigurationClass configClass, ConfigurationClassParser.SourceClass sourceClass, Predicate filter) throws IOException {
    2. //获取内部类
    3. Collection memberClasses = sourceClass.getMemberClasses();
    4. //如果存在内部类
    5. if (!memberClasses.isEmpty()) {
    6. List candidates = new ArrayList(memberClasses.size());
    7. Iterator var6 = memberClasses.iterator();
    8. ConfigurationClassParser.SourceClass candidate;
    9. while(var6.hasNext()) {
    10. candidate = (ConfigurationClassParser.SourceClass)var6.next();
    11. //内部类是否需要被注入IOC容器
    12. if (ConfigurationClassUtils.isConfigurationCandidate(candidate.getMetadata()) && !candidate.getMetadata().getClassName().equals(configClass.getMetadata().getClassName())) {
    13. //满足条件注入
    14. candidates.add(candidate);
    15. ......
    16. //递归方法,最终会再调用doProcessConfigurationClass
    17. this.processConfigurationClass(candidate.asConfigClass(configClass), filter);
    18. .......

    实操用法如下:

    1. @SpringBootApplication
    2. public class BookstoreApplication {
    3. public static void main(String[] args) {
    4. SpringApplication.run(new Class[]{BookstoreApplication.class, TestApp.class}, args);
    5. }
    6. @Component
    7. class TestClass{
    8. private final String name = "123";
    9. TestClass(){
    10. System.out.println(name);
    11. }
    12. }
    13. }

    如上,会通过方法processMemberClasses把TestClass信息注入IOC。

    1.2 @ComponentScan形式

    因为第一次进入doProcessConfigurationClass方法,传入的就是已经加入容器内的主启动类,所以会解析主启动类的注解@ComponentScan,大致和前文介绍的一样,如果@ComponentScan的basePackages和basePackageClasses属性都没有定义的话,就加载主启动类当前路径下各级文件夹中所有的class:

    1. Set componentScans = AnnotationConfigUtils.attributesForRepeatable(sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);
    2. if (!componentScans.isEmpty() && !this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {
    3. Iterator var14 = componentScans.iterator();
    4. while(var14.hasNext()) {
    5. AnnotationAttributes componentScan = (AnnotationAttributes)var14.next();
    6. Set scannedBeanDefinitions = this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());

    下一步需要进行判断,这些加载的class是否满足立即被加载进容器的条件,简单来说就是类上是否包含四个注解:

    [org.springframework.context.annotation.Import, org.springframework.stereotype.Component, org.springframework.context.annotation.ImportResource, org.springframework.context.annotation.ComponentScan]

    如果含有其中之一, 则使用递归调用的方式,重新调用ConfigurationClassParser类的parser方法:

    1. if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {
    2. this.parse(bdCand.getBeanClassName(), holder.getBeanName());
    3. }

    以这样的方式,把加入容器中的每一个类都当作了潜在的配置类,可以灵活的引用没有配置进容器的类。

    实操示例如下:

    1. @Configuration
    2. @ComponentScan("test")
    3. public class TestConfiguration {
    4. }

    当前类在com包下,被扫描进IOC容器,因为使用了@ComponentScan注解,可以加载外部包test下,标注了@Component的类:

    1. package test;
    2. import org.springframework.stereotype.Component;
    3. @Component
    4. public class TestComponentScan{
    5. TestComponentScan(){
    6. System.out.println("test----TestComponentScan if exist in IOC");
    7. }
    8. }

    1.3 @Import形式

    this.processImports(configClass, sourceClass, this.getImports(sourceClass), filter, true);

    把当前类中注解了@Import的类信息注册到容器中,注意这里有一种特殊情形需要注意,如果式主启动类,由于它通过@EnableAutoConfiguration复合注解,组合了@Import({AutoConfigurationImportSelector.class})注解,相当于要加载一些Spring自带的需要加入容器的类,这就属于特殊情形。

    Spring框架使AutoConfigurationImportSelector实现了一个特殊接口DeferredImportSelector,把它归属到特殊的一类Import,等当前工程项目中所有的类注册完成后再继续加载:

    1. if (selector instanceof DeferredImportSelector) {
    2. this.deferredImportSelectorHandler.handle(configClass, (DeferredImportSelector)selector);
    3. }

    此时,其他的普通类通过@Import注解依然通过递归调用的方式,可以对@Import其他引入的类进行继续解析: 

    this.processImports(configClass, currentSourceClass, importSourceClasses, exclusionFilter, false);

    1.4 @ImportResource形式

    根据注解信息,找到Spring的配置文件路径,之后加载配置信息,把配置的bean信息注册到IOC容器:

    importResource = AnnotationConfigUtils.attributesFor(sourceClass.getMetadata(), ImportResource.class);

    具体操作示例如下:

    1. @SpringBootApplication
    2. @ImportResource(locations={"classpath:beans.xml"})
    3. public class BookstoreApplication {
    4. public static void main(String[] args) {
    5. SpringApplication.run(BookstoreApplication.class, args);

     beans.xml具体信息如下,注册一个不在当前主启动类的bean:

    1. "1.0" encoding="UTF-8"?>
    2. "http://www.springframework.org/schema/beans"
    3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    4. xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    5. "helloService" class="test.HelloService">

     在项目启动后,HelloService就可以直接从容器中获取:

    1. public class HelloService {
    2. HelloService(){
    3. System.out.println("基于@ImportResource注解的bean--HelloService注入");
    4. }
    5. }

    1.5 beanMethods方式

    在实际的项目中,还有一种情况用的比较多,比如当前类标注了@Configuration,就代表是一个配置类,之后在方法中可以用@Bean注解的方式来把返回值注入到容器中。

    1. Set beanMethods = this.retrieveBeanMethodMetadata(sourceClass);
    2. Iterator var18 = beanMethods.iterator();
    3. while(var18.hasNext()) {
    4. MethodMetadata methodMetadata = (MethodMetadata)var18.next();
    5. configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass));
    6. }

    具体用法如下:

    1. @Configuration
    2. public class TestComponentScan{
    3. @Bean
    4. public TestBeanMethod testBeanMethod(){
    5. return new TestBeanMethod();
    6. }
    7. }

    1.6 解析接口中的beanMethods方式

    主要是针对当前类已经实现的接口中,其方法上,是否有需要注入IOC容器中的类:

    this.processInterfaces(configClass, sourceClass);

    示例如下:

    1. public interface InterfaceService {
    2. @Bean
    3. default Object interfaceBean(){
    4. return new Object();
    5. }
    6. }
    1. @Component
    2. public class CustomApplicationListener implements InterfaceService {
    3. }

     CustomApplicationListener  会在自身注入容器的同时,把InterfaceService中注入了@Bean方法interfaceBean也加入进容器。

    其代码实现为:

    1. private void processInterfaces(ConfigurationClass configClass, ConfigurationClassParser.SourceClass sourceClass) throws IOException {
    2. Iterator var3 = sourceClass.getInterfaces().iterator();
    3. while(var3.hasNext()) {
    4. ConfigurationClassParser.SourceClass ifc = (ConfigurationClassParser.SourceClass)var3.next();
    5. Set beanMethods = this.retrieveBeanMethodMetadata(ifc);
    6. Iterator var6 = beanMethods.iterator();
    7. while(var6.hasNext()) {
    8. MethodMetadata methodMetadata = (MethodMetadata)var6.next();
    9. if (!methodMetadata.isAbstract()) {
    10. configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass));
    11. }
    12. }
    13. this.processInterfaces(configClass, ifc);
    14. }
    15. }

    这些beanMethods的bean定义信息会依附于当前类bean信息而存在,具体表现为存在ConfigurationClass类的Set属性beanMethods中,在需要的时候解析出来。

    1.7 递归解析当前类的父类

    现在到了最后一步,首先判断这个类是否还有父类,如果没有,直接返回空,如果有,获取父类后,继续判断,如果这个父类不为空、包名开头不是java且不能是已经被解析的父类,否则返回空:

    1. if (sourceClass.getMetadata().hasSuperClass()) {
    2. String superclass = sourceClass.getMetadata().getSuperClassName();
    3. if (superclass != null && !superclass.startsWith("java") && !this.knownSuperclasses.containsKey(superclass)) {
    4. this.knownSuperclasses.put(superclass, configClass);
    5. return sourceClass.getSuperClass();
    6. }
    7. }
    8. return null;

    取得返回值后,跳出循环,把当前ConfigurationClass返回,缓存到Map中:

    1. do {
    2. sourceClass = this.doProcessConfigurationClass(configClass, sourceClass, filter);
    3. } while(sourceClass != null);
    4. this.configurationClasses.put(configClass, configClass);

    经过ConfigurationClassParser类中的parse方法调用AnnotatedBeanDefinition类型的parse方法,我们完成了对当前项目工程下所有类,注册到IOC容器中的操作。

    2 Spring自动装配原理

    下面需要继续介绍,Spring自带的框架类,如何加入容器。在ConfigurationClassParser类中的执行方法为:

    this.deferredImportSelectorHandler.process();

    首先会获取到前面我们通过主启动类上的@Import注解导入的自动装配类信息,之后新建一个Group用来对这一类信息归类,把@Import中的信息导入到Group,之后对Group中的信息进行处理:

    1. public void process() {
    2. List deferredImports = this.deferredImportSelectors;
    3. this.deferredImportSelectors = null;
    4. try {
    5. if (deferredImports != null) {
    6. ......
    7. deferredImports.forEach(handler::register);
    8. handler.processGroupImports();
    9. }
    10. } finally {
    11. this.deferredImportSelectors = new ArrayList();
    12. }
    13. }
    1. public void processGroupImports() {
    2. Iterator var1 = this.groupings.values().iterator();
    3. while(var1.hasNext()) {
    4. ConfigurationClassParser.DeferredImportSelectorGrouping grouping = (ConfigurationClassParser.DeferredImportSelectorGrouping)var1.next();
    5. Predicate exclusionFilter = grouping.getCandidateFilter();
    6. grouping.getImports().forEach((entry) -> {
    7. ConfigurationClass configurationClass = (ConfigurationClass)this.configurationClasses.get(entry.getMetadata());

    最后通过grouping.getImports()方法来调用主启动类导入的类AutoConfigurationImportSelector中的process方法,来进行自动装配:

    1. public Iterable getImports() {
    2. Iterator var1 = this.deferredImports.iterator();
    3. while(var1.hasNext()) {
    4. ConfigurationClassParser.DeferredImportSelectorHolder deferredImport = (ConfigurationClassParser.DeferredImportSelectorHolder)var1.next();
    5. this.group.process(deferredImport.getConfigurationClass().getMetadata(), deferredImport.getImportSelector());
    6. }

    进入AutoConfigurationImportSelector:

    1. public void process(AnnotationMetadata annotationMetadata, DeferredImportSelector deferredImportSelector) {
    2. Assert.state(deferredImportSelector instanceof AutoConfigurationImportSelector, () -> {
    3. return String.format("Only %s implementations are supported, got %s", AutoConfigurationImportSelector.class.getSimpleName(), deferredImportSelector.getClass().getName());
    4. });
    5. AutoConfigurationImportSelector.AutoConfigurationEntry autoConfigurationEntry = ((AutoConfigurationImportSelector)deferredImportSelector).getAutoConfigurationEntry(this.getAutoConfigurationMetadata(), annotationMetadata);

    获取配置实体: 

    1. protected AutoConfigurationImportSelector.AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata, AnnotationMetadata annotationMetadata) {
    2. if (!this.isEnabled(annotationMetadata)) {
    3. return EMPTY_ENTRY;
    4. } else {
    5. AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
    6. List configurations = this.getCandidateConfigurations(annotationMetadata, attributes);

     执行扫描方法:

    1. protected List getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
    2. List configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader());
    3. Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you are using a custom packaging, make sure that file is correct.");
    4. return configurations;
    5. }

     通过this.getSpringFactoriesLoaderFactoryClass()方法扫描META-INF/spring.factories中key为EnableAutoConfiguration的全类名:

    1. protected Class getSpringFactoriesLoaderFactoryClass() {
    2. return EnableAutoConfiguration.class;
    3. }

    Spring框架的示例配置为:

    1. # Auto Configure
    2. org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
    3. org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\

     以上就是Spring自动装配的全部内容。

    到了这一步,我们才讲完了把所有需要注册进容器的类缓存到一个Map集合,下一步就是取到Map集合的值,然后把值信息作为bean的定义信息,真正注册到容器中:

    1. Set configClasses = new LinkedHashSet(parser.getConfigurationClasses());
    2. configClasses.removeAll(alreadyParsed);
    3. if (this.reader == null) {
    4. this.reader = new ConfigurationClassBeanDefinitionReader(registry, this.sourceExtractor, this.resourceLoader, this.environment, this.importBeanNameGenerator, parser.getImportRegistry());
    5. }
    6. this.reader.loadBeanDefinitions(configClasses);

    这样,就完成了ConfigurationClassPostProcessor类中,第二次BeanDefinitionRegistry后置处理器中指定方法的执行:

    1. invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry);
    2. currentRegistryProcessors.clear();

    执行完成后,清空currentRegistryProcessors,为下一次执行BeanDefinitionRegistry后置处理器方法做准备。

    3 小结

    想必很多读者对主启动类下包扫描,和Springboot自动装配原理都不陌生,这里只是给大家提供了一个源码级别的验证,让大家能够更清晰而完整的了解Springboot的一些原理。本文也结合了一些具体示例,希望对大家有所帮助。由于笔者水平有限,有不足之处,也望大家多多指教、包涵!

  • 相关阅读:
    论<script> 标签可以直接写在 HTML 文件中的哪些位置?(可以将 <script> 标签直接插入到 HTML 文件的任何位置)
    (23)语义分割--UNet
    基于遗传算法与神经网络的测井预测(Matlab代码实现)
    使用shapely判断坐标点位于哪个路区
    Dockerfile构建镜像
    【云原生 | Kubernetes 系列】--Ceph Dashboard和Ceph 监控
    uniapp cli化一键游项目启动报错总结
    Mybatis实战练习四【单个条件(动态SQL)&添加数据】
    Lumos-az/MiniSQL阅读笔记
    redis集群理论和搭建
  • 原文地址:https://blog.csdn.net/bigbearxyz/article/details/126779495