Springboot自动装配源码及启动原理理解

springboot版本:2.2.2

传统的Spring框架实现一个Web服务,需要导入各种依赖JAR包,然后编写对应的XML配置文件 等,相较而言,Spring Boot显得更加方便、快捷和高效。那么,Spring Boot究竟如何做到这些的呢? 接下来分别针对Spring Boot框架的依赖管理、自动配置和执行流程进行深入分析

依赖管理

首先,创建一个springboot工程实现web服务,pom.xml文件有两个核心依赖,分别是 spring-boot-starter-parentspring-boot-starter-web

  • spring-boot-starter-parent

    在pom.xml中找到对应的依赖如下

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.5</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    上述代码中,将spring-boot-starter-parent依赖作为Spring Boot项目的统一父项目依赖管理,并 将项目版本号统一为2.7.5,该版本号根据实际开发需求是可以修改的

    使用“Ctrl+鼠标左键”进入并查看spring-boot-starter-parent底层源文件,发现spring-boot-starter-parent的底层有一个父依赖spring-boot-dependencies,核心代码具体如下

    <parent>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-dependencies</artifactId>
      <version>2.7.5</version>
    </parent>

    发现还有爷爷辈,继续查看底层源文件,核心代码如下

    <properties>
      <activemq.version>5.16.5</activemq.version>
      <antlr2.version>2.7.7</antlr2.version>
      <appengine-sdk.version>1.9.98</appengine-sdk.version>
      <artemis.version>2.19.1</artemis.version>
      <aspectj.version>1.9.7</aspectj.version>
      <assertj.version>3.22.0</assertj.version>
      <atomikos.version>4.0.6</atomikos.version>
      <awaitility.version>4.2.0</awaitility.version>
      <build-helper-maven-plugin.version>3.3.0</build-helper-maven-plugin.version>
      <byte-buddy.version>1.12.18</byte-buddy.version>
      <cache2k.version>2.6.1.Final</cache2k.version>
      ...
    </properties>

    可以发现,该文件通过标签对一些常用技术框架的依赖文件 进行了统一版本号管理,说明pom.xml引入依赖文件不需要标注依赖文件版本号

    思考一下,spring-boot-starter-parent父依赖启动器的主要作用是进行版本统一管理,那么项目运行依赖的JAR包是从何而来的?

  • spring-boot-starter-web

    查看spring-boot-starter-web依赖文件源码,核心代码具体如下

    <dependencies>
        <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter</artifactId>
          <version>2.7.5</version>
          <scope>compile</scope>
        </dependency>
        <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-json</artifactId>
          <version>2.7.5</version>
          <scope>compile</scope>
        </dependency>
        <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-tomcat</artifactId>
          <version>2.7.5</version>
          <scope>compile</scope>
        </dependency>
        <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-web</artifactId>
          <version>5.3.23</version>
          <scope>compile</scope>
    </dependency>

    从上述代码可以发现,spring-boot-starter-web依赖启动器的主要作用是提供Web开发场景所需的底层 所有依赖

    正是如此,在pom.xml中引入spring-boot-starter-web依赖启动器时,就可以实现Web场景开发,而 不需要额外导入Tomcat服务器以及其他Web依赖文件等。当然,这些引入的依赖文件的版本号还是由 spring-boot-starter-parent父依赖进行的统一管理。


自动配置(启动流程)

概念:能够在我们添加jar包依赖的时候,自动为我们配置一些组件的相关配置,我们无需配置或者只需 要少量配置就能运行编写的项目

那Spring Boot到底是如何进行自动配置的,都把哪些组件进行了自动配置?

自问自答:Spring Boot应用的启动入口是@SpringBootApplication注解标注类中的main()方法, @SpringBootApplication能够扫描Spring组件并自动配置Spring Boot;自定义了一个demo,查看@SpringBootApplication内部源码进行分析 ,核心代码具体如下:

@SpringBootApplication
public class SpringbootDemoApplication {
​
    public static void main(String[] args) {
        SpringApplication.run(SpringbootDemoApplication.class, args);
    }
​
}

点进去看一下 @SpringBootApplication 有什么

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration                  //表明该类为配置类
@EnableAutoConfiguration                  //启动自动配置功能
@ComponentScan(                           //包扫描器
    excludeFilters = {
    @Filter(type = FilterType.CUSTOM,classes = {TypeExcludeFilter.class}),  
    @Filter(type = FilterType.CUSTOM,classes = {AutoConfigurationExcludeFilter.class})
    }
)
public @interface SpringBootApplication {...}

@SpringBootConfiguration :表示Spring Boot配置类。查看@SpringBootConfiguration注解源 码,核心代码具体如下。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
@Indexed
public @interface SpringBootConfiguration {...}

从上述源码可以看出,@SpringBootConfiguration 注解内部有一个核心注解 @Configuration,该注解是Spring框架提供的,表示当前类为一个配置类(XML配置文件的注解表现形式),并可以被组件扫描器扫描。由此可见,@SpringBootConfiguration 注解的作用与 @Configuration 注解相同,都是标识一个可以被组件扫描器扫描的配置类,只不过 @SpringBootConfiguration 是被SpringBoot进行了重新封装命名而已

重点关注一下 @EnableAutoConfiguration:表示开启自动配置功能,该注解是SpringBoot框架最重要的注解,也是实现自动化配置的注解。同样,查看该注解内部查看源码信息,核心代码具体如下

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage                         //自动配置包 
@Import(AutoConfigurationImportSelector.class)    //借助@Import注解来收集所有符合自动配置条件的bean定义,并加载到IoC容器
public @interface EnableAutoConfiguration {...}

可以发现它是一个组合注解,Spring 中有很多以Enable开头的注解,其作用就是借助@Import来 收集并注册特定场景相关的bean,并加载到IoC容器。@EnableAutoConfiguration 就是借助@Import 来收集所有符合自动配置条件的bean定义,并加载到IoC容器。下面,对这两个核心注解分别讲解 :

@AutoConfigurationPackage: 会把 @SpringBootApplication 注解标注的类所在的包名拿到,并且对该包及其子包进行扫描,将组件添加到容器中;

@Target(ElementType.TYPE) 
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
​
//@Import为Spring框架底层注解,它的作用就是给容器中导入某个组件类,
@Import(AutoConfigurationPackages.Registrar.class) //默认将主配置类(@SpringbootAppication)所在的包及其子包里面所有的组件扫描到容器中;
public @interface AutoConfigurationPackage {...}

从上述源码可以看出,@AutoConfigurationPackage 注解的功能是由 @Import 注解实现的,它是 spring框架的底层注解,它的作用就是给容器中导入某个组件类;

@Import(AutoConfigurationPackages.Registrar.class):可以帮助springboot应用将所有符合条件的@Configuration配置都加载到当前Springboot创建并使用的IoC容器中。

从上述源码可以看出,在Registrar类中有一个 registerBeanDefinitions() 方法,使用Debug模式启动项目,可以看到选中的部分就是com.yun。也就是说,@AutoConfigurationPackage 注解的主要作用就是将主程序类所在包及所有子包下的组件到扫描到spring容器中。因此在定义项目包结构时,要求定义的包结构非常规范,项目主程序启动类要定义在最外层的根目录位置,然后在根目录位置内部建立子包和类进行业务开发,这样才能够保证定义的类能够被组件扫描器扫 描;

@Import({AutoConfigurationImportSelector.class}):AutoConfigurationImportSelector 这个类导入到spring容器中, AutoConfigurationImportSelector 可以帮助springboot应用将所有符合条件的 @Configuration 配置都加载到当前SpringBoot创建并使用的IoC容器(ApplicationContext)中;

继续研究 AutoConfigurationImportSelector 这个类,通过源码分析这个类中是通过 selectImports 这 个方法告诉springboot都需要导入那些组件:

进入 loadMetData() 方法

进入 getAutoConfigurationEntry() 在进入 getCandidateConfigurations() 方法

该方法中有一个重要方法loadFactoryNames,这个方法是让SpringFactoryLoader去加载一些组件的 名字。

继续点开loadFactory方法

会去读取一个 spring.factories 的文件,读取不到会表这个错误,我们继续根据会看到,最终路径的长 这样,而这个是spring提供的一个工具类

它其实是去加载一个外部的文件,而这文件是在

@EnableAutoConfiguration 就是从classpath中搜寻META-INF/spring.factories配置文件,并将其中 org.springframework.boot.autoconfigure.EnableutoConfiguration 对应的配置项通过反射(Java Refletion)实例化为对应的标注了 @Configuration 的 JavaConfig形式的配置类,并加载到IOC容器中;

总结
springboot底层实现自动配置的步骤是:
  1. springboot应用启动;

  2. @SpringBootApplication起作用;

  3. @EnableAutoConfiguration;

  4. @AutoConfigurationPackage:这个组合注解主要是 @Import(AutoConfigurationPackages.Registrar.class),它通过将Registrar类导入到容器中,而 Registrar类作用是扫描主配置类同级目录以及子包,并将相应的组件导入到springboot创建管理 的容器中;

  5. @Import(AutoConfigurationImportSelector.class):它通过将 AutoConfigurationImportSelector类导入到容器中,AutoConfigurationImportSelector类作用 是通过selectImports方法执行的过程中,会使用内部工具类SpringFactoriesLoader,查找 classpath上所有jar包中的META-INF/spring.factories进行加载,实现将配置类信息交给 SpringFactory加载器进行一系列的容器创建过程

@ComponentScan:具体扫描的包的根路径由Spring Boot项目主程序启动类所在包位置决 定,在扫描过程中由前面介绍的 @AutoConfigurationPackage 注解进行解析,从而得到Spring Boot项 目主程序启动类所在包的具体位置


自定义starter

  • 新建maven工程 工程名为wsy-spring-boot-starter

    <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-autoconfigure</artifactId>
                <version>2.3.12.RELEASE</version>
            </dependency>
  • 编写javaBean

    @Data
    @EnableConfigurationProperties(SimpleBean.class) //开启@ConfigurationProperties
    @ConfigurationProperties(prefix = "simplebean")  //prefix命名全部为小写
    public class SimpleBean {
    ​
        private String id;
    ​
        private String name;
    }
  • 编写配置类 MyAutoConfiguration

    @Configuration
    @ConditionalOnClass  //当类路径class下有指定的类的情况,就会进行自动配置
    public class MyAutoConfiguration {
    ​
        static {
            System.out.println("MyAutoConfiguration init...");
        }
    ​
        @Bean
        public SimpleBean simpleBean(){
            return new SimpleBean();
        }
    ​
    }
  • resource下创建/META-INF/spring.factories

    org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
    com.yun.config.MyAutoConfiguration

使用starter

  • 在之前的springboot-demo项目的pom.xml中引入自定义starter依赖

    <dependency>
         <groupId>com.yun</groupId>
         <artifactId>wsy-spring-boot-starter</artifactId>
         <version>1.0-SNAPSHOT</version>
    </dependency>
  • 在全局配置文件中配置属性值

    simplebean.id=1
    simplebean.name=wsy
  • 编写测试方法

    @Autowired
    private SimpleBean simpleBean;
    ​
    @Test
    public void wsyStarterTest(){
       System.out.println(simpleBean);
    }
  • 查询结果

    MyAutoConfiguration init...
    2022-12-01 16:21:54.168  INFO 13996 --- [           main] c.y.s.SpringbootDemoApplicationTests     : Started SpringbootDemoApplicationTests in 4.143 seconds (JVM running for 6.581)
    SimpleBean{id='1', name='wsy'}

执行原理

每个Spring Boot项目都有一个主程序启动类,在主程序启动类中有一个启动项目的main()方法, 在该方法中通过执行SpringApplication.run()即可启动整个Spring Boot程序。

那么SpringApplication.run()方法到底是如何做到启动Spring Boot项目的呢?

带着疑问我们debug走一遍内部源码查看逻辑;

@SpringBootApplication
public class SpringbootDemoApplication {
​
    public static void main(String[] args) {
        SpringApplication.run(SpringbootDemoApplication.class, args);
    }
​
}
public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
    return run(new Class[]{primarySource}, args);
}
​
public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
    return (new SpringApplication(primarySources)).run(args);
}

从上述源码可以看出,SpringApplication.run() 方法内部执行了两个操作,分别是 SpringApplication实例的初始化创建和调用run()启动项目,这两个阶段的实现具体说明如下

  1. SpringApplication实例的初始化创建

    查看SpringApplication实例对象初始化创建的源码信息,核心代码具体如下

    public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
            this.sources = new LinkedHashSet();
            this.bannerMode = Mode.CONSOLE;
            this.logStartupInfo = true;
            this.addCommandLineProperties = true;
            this.addConversionService = true;
            this.headless = true;
            this.registerShutdownHook = true;
            this.additionalProfiles = Collections.emptySet();
            this.isCustomEnvironment = false;
            this.lazyInitialization = false;
            this.applicationContextFactory = ApplicationContextFactory.DEFAULT;
            this.applicationStartup = ApplicationStartup.DEFAULT;
            this.resourceLoader = resourceLoader;
            Assert.notNull(primarySources, "PrimarySources must not be null");
            //把项目启动类.class设置为属性存储起来
            this.primarySources = new LinkedHashSet(Arrays.asList(primarySources));
            //判断当前webApplicationType应用的类型
            this.webApplicationType = WebApplicationType.deduceFromClasspath();
            this.bootstrapRegistryInitializers = new ArrayList(this.getSpringFactoriesInstances(BootstrapRegistryInitializer.class));
            //设置初始化器(Initializer),最后会调用这些初始化器
            this.setInitializers(this.getSpringFactoriesInstances(ApplicationContextInitializer.class));
            // 设置监听器(Listener)
            this.setListeners(this.getSpringFactoriesInstances(ApplicationListener.class));
            //用于推断并设置项目main()方法启动的主程序启动类
            this.mainApplicationClass = this.deduceMainApplicationClass();
        }

    从上述源码可以看出,SpringApplication的初始化过程主要包括4部分,具体说明如下。

    1. this.webApplicationType = WebApplicationType.deduceFromClasspath() 用于判断当前webApplicationType应用的类型。deduceFromClasspath() 方法用于查看Classpath类路 径下是否存在某个特征类,从而判断当前webApplicationType类型是SERVLET应用(Spring 5之前的传 统MVC应用)还是REACTIVE应用(Spring 5开始出现的WebFlux交互式应用)

    2. this.setInitializers(this.getSpringFactoriesInstances(ApplicationContextInitializer.class)) 用于SpringApplication应用的初始化器设置。在初始化器设置过程中,会使用Spring类加载器 SpringFactoriesLoader从META-INF/spring.factories类路径下的META-INF下的spring.factores文件中 获取所有可用的应用初始化器类 ApplicationContextInitializer

    3. this.setListeners(this.getSpringFactoriesInstances(ApplicationListener.class)) 用于SpringApplication应用的监听器设置。监听器设置的过程与上一步初始化器设置的过程基本一样, 也是使用SpringFactoriesLoader从META-INF/spring.factories类路径下的META-INF下的 spring.factores文件中获取所有可用的监听器类 ApplicationListener

    4. this.mainApplicationClass = this.deduceMainApplicationClass() 用于推断并设置项目main()方法启动的主程序启动类

  2. 项目的初始化启动

    分析完 (new SpringApplication(primarySources)).run(args) 源码前一部分SpringApplication实例对象的初始化创建后,查看run(args)方法执行的项目初始化启动过程,核心代码具体如下:

    public ConfigurableApplicationContext run(String... args) {
       long startTime = System.nanoTime();
       DefaultBootstrapContext bootstrapContext = createBootstrapContext();
       // 初始化应用上下文和异常报告集合
       ConfigurableApplicationContext context = null;
       // 配置headless属性
       configureHeadlessProperty();
        // 第一步:获取并启动监听器
       SpringApplicationRunListeners listeners = getRunListeners(args);
       listeners.starting(bootstrapContext, this.mainApplicationClass);
       try {
          // 创建 ApplicationArguments 对象 初始化默认应用参数类
          // args是启动Spring应用的命令行参数,该参数可以在Spring应用中被访问 如 -- server.port=8080
          ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
          // 第二步:根据SpringApplicationRunListeners以及参数来准备环境
          // 创建并配置当前SpringBoot应用将要使用的Environment
          // 并遍历调用所有的SpringApplicationRunListener的environmentPrepared()方法
          ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
          configureIgnoreBeanInfo(environment);
          // 准备Banner打印器 - 就是启动Spring Boot的时候打印在console上的ASCII艺术字体
          Banner printedBanner = printBanner(environment);
          // 第三步:创建Spring容器
          context = createApplicationContext();
          context.setApplicationStartup(this.applicationStartup);
          // 第四步:Spring容器前置处理
          // 主要是在容器刷新之前的准备动作,包含一个非常关键的操作;将启动类注入容器,为后续开启自动化配置奠定基础。
          prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
          // 第五步:刷新容器
          refreshContext(context);
          // 第六步:Spring容器后置处理
          afterRefresh(context, applicationArguments);
          Duration timeTakenToStartup = Duration.ofNanos(System.nanoTime() - startTime);
          if (this.logStartupInfo) {
             new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), timeTakenToStartup);
          }
          // 第七步:发出结束执行的事件
          listeners.started(context, timeTakenToStartup);
          // 第八步:执行Runners
          // Runners 运行器用于在服务启动时进行一些业务初始化操作,这些操作只在服务器启动后执行一次
          // SpringBoot提供了ApplicationRunner和COmmandLineRunner两种服务接口
          callRunners(context, applicationArguments);
       }
       catch (Throwable ex) {
          // 如果发生异常,则进行处理,并抛出 IllegalStateException 异常
          handleRunFailure(context, ex, listeners);
          throw new IllegalStateException(ex);
       }
       // 发布应用上下文就绪事件
       // 表示在前面一切初始化启动都没有问题的情况下,使用运行监听器 SpringApplicationRunListener 持续运行配置好的应用上下文 ApplicationContext,
       // 这样整个SpringBoot项目就正式启动完成了
       try {
          Duration timeTakenToReady = Duration.ofNanos(System.nanoTime() - startTime);
          listeners.ready(context, timeTakenToReady);
       }
       catch (Throwable ex) {
          // 如果发生异常,则进行处理,并抛出 IllegalStateException 异常
          handleRunFailure(context, ex, null);
          throw new IllegalStateException(ex);
       }
       //返回容器
       return context;
    }

    从上述源码可以看出,项目初始化启动过程大致包括以下部分:

    第一步:获取并启动监听器

    this.getRunListeners(args)和listeners.starting()方法主要用于获取SpringApplication实例初始化过程中初始化的SpringApplicationRunListener监听器并运行。

    第二步:根据SpringApplicationRunListeners以及参数来准备环境

    prepareEnvironment(listeners, bootstrapContext, applicationArguments)方法主要用于对项目运行环境进行预设置,同时通过this.configureIgnoreBeanInfo(environment)方法排除一些不需要的运行环境

    第三步:创建Spring容器

    根据webApplicationType进行判断, 确定容器类型,如果该类型为SERVLET类型,会通过反射装载对应的字节码,也就是AnnotationConfigServletWebServerApplicationContext,接着使用之前初始化设置的context(应用上下文环境)、environment(项目运行环境)、listeners(运行监听器)、applicationArguments(项目参数)和printedBanner(项目图标信息)进行应用上下文的组装配置,并刷新配置

    第四步:Spring容器前置处理

    这一步主要是在容器刷新之前的准备动作。设置容器环境,包括各种变量等等,其中包含一个非常关键的操作:将启动类注入容器,为后续开启自动化配置奠定基础

    第五步:刷新容器

    开启刷新spring容器,通过refresh方法对整个IOC容器的初始化(包括bean资源的定位,解析,注册等等),同时向JVM运行时注册一个关机钩子,在JVM关机时会关闭这个上下文,除非当时它已经关闭

    第六步:Spring容器后置处理

    扩展接口,设计模式中的模板方法,默认为空实现。如果有自定义需求,可以重写该方法。比如打印一些启动结束log,或者一些其它后置处理。

    第七步:发出结束执行的事件

    获取EventPublishingRunListener监听器,并执行其started方法,并且将创建的Spring容器传进去了,创建一个ApplicationStartedEvent事件,并执行ConfigurableApplicationContext 的publishEvent方法,也就是说这里是在Spring容器中发布事件,并不是在SpringApplication中发布事件,和前面的starting是不同的,前面的starting是直接向SpringApplication中的监听器发布启。

    第八步:执行Runners

    用于调用项目中自定义的执行器XxxRunner类,使得在项目启动完成后立即执行一些特定程序。其中,SpringBoot提供的执行器接口有ApplicationRunner 和CommandLineRunner两种,在使用时只需要自定义一个执行器类实现其中一个接口并重写对应的run()方法接口,然后SpringBoot项目启动后会立即执行这些特定程序。
最后绘制一个Spring Boot执行流程图便于理解