SpringBoot启动过程和自动装配


SpringBoot看过很多次,但是一直理解不深,只停留在了解的阶段。直到最近公司项目做拆分,需要用到SpringBoot启动时的一些特性,于是把启动过程看了一遍。举个例子

测试对Spring启动原理的理解

  • Rpc框架和Spring的集成问题。Rpc框架何时注册暴露服务,在哪个Spring扩展点注册呢?init-method 中行不行?
  • MQ 消费组和Spring的集成问题。MQ消费者何时开始消费,在哪个Spring扩展点”注册“自己?init-method 中行不行?
  • SpringBoot 集成Tomcat问题。如果出现已开启Http流量,Spring还未启动完成,怎么办?Tomcat何时开启端口,对外服务?

SpringBoot项目常见的流量入口无外乎 Rpc、Http、MQ 三种方式。一名合格的架构师必须精通服务的入口流量何时开启,如何正确开启?

SpringBoot是什么

我们日常开发对SpringBoot的使用,可谓非常熟练了。但是很多SpringBoot的很多东西对我们来说依然如同一个黑盒。例如SpringBoot的自动装配,有注解可以实现注入为什么还需要SPI。springboot是依赖于spring的,比起spring,除了拥有spring的全部功能以外,springboot无需繁琐的xml配置,这取决于它自身强大的自动装配功能;并且自身已嵌入Tomcat、Jetty等web容器,集成了springmvc,使得springboot可以直接运行,不需要额外的容器,提供了一些大型项目中常见的非功能性特性、指标、健康检测、外部配置等,

其实spring大家都知道,boot是启动的意思。所以,spring boot其实就是一个启动spring项目的一个工具而已,总而言之,springboot 是一个服务于框架的框架;也可以说springboot是一个工具,这个工具简化了spring的配置;

SpringBoot的启动过程

一个spring应用的启动从run方法开始

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

点进源来,SpringApplication类中有个静态run()方法,最终执行如下:

public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
        return (new SpringApplication(primarySources)).run(args);
    }

显然到这里分为两步,下面分析一下这两步。其中第一步是一些准备工作,Spring的核心启动流程在第二步

  1. 实例化new SpringApplication()。
  2. 执行SpringApplication的run()方法。

1.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");
        this.primarySources = new LinkedHashSet(Arrays.asList(primarySources));
        this.webApplicationType = WebApplicationType.deduceFromClasspath();
        this.bootstrapRegistryInitializers = this.getBootstrapRegistryInitializersFromSpringFactories();
        // 加载初始器,配置应用程序启动前的初始化对象 
    this.setInitializers(this.getSpringFactoriesInstances(ApplicationContextInitializer.class));
        // 加载监听器
        this.setListeners(this.getSpringFactoriesInstances(ApplicationListener.class));
        this.mainApplicationClass = this.deduceMainApplicationClass();
    }

构造函数中重要的两个方法就是初始化和见监听器,初始化器以及监听器 调用的方法都是 getSpringFactoriesInstances
最终逻辑是从类路径META-INF/spring.factories中 加载 初始化器以及监听器 。

  • 初始化器(ApplicationContextInitializer): 初始化某些 IOC容器刷新之前的组件。
  • 监听器(ApplicationListener): 监听特定的事件 比如IOC容器刷新、容器关闭等。

getSpringFactoriesInstances() 方法根据指定的Class类型从META-INF/spring.factories 中获取相应的自动配置类。

private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
    ClassLoader classLoader = this.getClassLoader();
    // 从 META-INF/spring.factories 中获取  (loadFactoryNames方法)
    Set<String> names = new LinkedHashSet(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
    // 根据名称实例化
    List<T> instances = this.createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
    AnnotationAwareOrderComparator.sort(instances);
    return instances;
}

SpringApplication的构建都是为了run()方法启动做铺垫,最重要的部分就是设置应用类型、设置初始化器、设置监听器

注意: 初始化器和这里的监听器都要放置在spring.factories文件中才能在这一步骤加载,否则不会生效。因为此时IOC容器还未创建,所以即使将其注入到IOC容器中也是不会生效的。

2.SpringApplication的run方法

SpringApplication构建完成,一切都做好了铺垫,现在到了启动的过程了。下面是run方法的源码,我这里用的是spring-boot-starter-parent的 2.5.3 版本,不同版本源码差异可能会稍有差别

public ConfigurableApplicationContext run(String... args) {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        DefaultBootstrapContext bootstrapContext = this.createBootstrapContext();
        ConfigurableApplicationContext context = null;
        this.configureHeadlessProperty();
        // 获取监听器,就是在第一步SpringApplication构造函数中加载的监听器
        SpringApplicationRunListeners listeners = this.getRunListeners(args);
        // 通知监听者Spring启动开始
        listeners.starting(bootstrapContext, this.mainApplicationClass);

        try {
            ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
            // 创建应用程序环境 配置文件在此处读取(application.properties application.yml)
            ConfigurableEnvironment environment = this.prepareEnvironment(listeners, bootstrapContext, applicationArguments);
            this.configureIgnoreBeanInfo(environment);
            // 控制台打印 Banner
            Banner printedBanner = this.printBanner(environment);
            // 2.创建应用程序上下文...此处创建了beanfactory,创建IoC容器
            context = this.createApplicationContext();
            context.setApplicationStartup(this.applicationStartup);
            // 准备ApplicationContext IOC容器基本信息
            this.prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
            // 3.刷新上下文(spring启动核心,刷新IOC容器refresh,tomcat,bean组件的初始化都在这一步完成)
            this.refreshContext(context);
            // 刷新IOC容器的后置处理,源码是空方法,如果有自定义需求可以重写,比如打印一些启动结束日志等。
            this.afterRefresh(context, applicationArguments);
            stopWatch.stop();
            if (this.logStartupInfo) {
                (new StartupInfoLogger(this.mainApplicationClass)).logStarted(this.getApplicationLog(), stopWatch);
            }
            // 4.启动完成通知
            listeners.started(context);
            // 执行自定义的run方法,源码是空方法,是一个扩展功能
            this.callRunners(context, applicationArguments);
        } catch (Throwable var10) {
            this.handleRunFailure(context, var10, listeners);
            throw new IllegalStateException(var10);
        }

        try {
            listeners.running(context);
            return context;
        } catch (Throwable var9) {
            this.handleRunFailure(context, var9, (SpringApplicationRunListeners)null);
            throw new IllegalStateException(var9);
        }
    }

可以看到源码中有很多空方法,是Spring给我们留的口子方便我们自定义扩展功能。总的流程大致如下

1、StopWatch保存一些信息,应用名字,当前启动时间

2、this.createBootstrapContext(),创建引导上下文

  2.1:获取到所有之前创建的bootstrapers,挨个执行initialize方法,来完成对引导启动器上下文环境设置。

3、this.configureHeadlessProperty(),让当前应用进入headless模式(用于在缺失显示屏、鼠标、键盘时候的系统配置)

4、this.getRunListeners(args),获取所有RunListener运行时监听器(为了方便所有Listener进行事件感知,项目在启动)

  4.1 getSpringFactoriesInstances去Spring.factories找SpringApplicationRunListener  

  4.2 遍历所有SpringApplicationRunListener,调用starting方法,监听所有项目运行的状态

  4.3 保存命令行参数;

  4.4 this.prepareEnvironment(…),准备环境

   4.4.1返回创建基础环境信息。StandardServletEnviroment

   4.4.2配置环境信息对象(properties等)

     4.4.2.1 读取所有配置元的配置属性

   4.4.3绑定环境信息

   4.4.4监听器调用environmentPrepared;通知所有监听器,当前环境准备完成。

  4.5 this.createApplicationContext(),创建IoC容器

   4.5.1根据项目类型(Servlet)创建容器

    4.5.2当前会创建AnnotationConfigServletWebServerApplicationContext;

  4.6 this.prepareContext(…)准备ApplicationContext IOC容器基本信息 prepareContext方法)

   4.6.1 保存环境信息

   4.6.2 IOC容器的后置处理流程

   4.6.3 应用初始化;applyInitializers

    4.6.3.1 遍历所有的ApplicationContxtInitializer,调用initialize,来对IOC容器进行初始化扩展

   4.7.4 遍历所有的Listener调用ContextPrepared。EventPublishListener;通知所有的监听器ContextPrepared

   4.7.5 所有监听器 调用contextLoaded,通知上下文IOC容器加载完成。

​ 4.7 this.refreshContext(context),刷新IOC容器refresh(核心源码)

    4.7.1创建容器中的所有组件

  4.8 this.afterRefresh(…),刷新容器后的工作

    4.8.1所有监听器调用started方法,当前项目启动了

  4.9 this.callRunners(…),调用runner方法

    4.9.1获取容器中的ApplicationRunner

    4.9.2获取容器中CommandLineRunner

    4.9.3合并所有runner,并且按照@order进行排序

    4.9.4遍历所有的runner调用run方法。

  4.10 this.handleRunFailure(…),如果有异常会调用Listener的failed方法

  4.11 listeners.running(context),调用所有监听器running方法Listener.running方法

  4.12 this.handleRunFailure(…),方法如果有异常,还是调用Listener的failed方法。

下面着重看一下核心方法this.refreshContext(context),中间件,tomcat,MQ,还有自己定义的Bean,依赖注入都是在这一步完成的。即使点开这个方法也只有几行。但里面做了很多事情

private void refreshContext(ConfigurableApplicationContext context) {
        if (this.registerShutdownHook) {
            shutdownHook.registerApplicationContext(context);
        }

        this.refresh(context);
    }

我们来看下这么几行代码,Spring是如何优雅的把容器Tomcat,中间件,自定义组件这么复杂的应用启起来的,这就是SpringBoot的精髓,自动装配

SpringBoot的自动装配

在讲之前先了解一下,手动装配的流程。在SpringMVC的时代,首先你需要引入Spring框架的相关依赖,包括核心容器(spring-context)、AOP(spring-aop)、数据访问(spring-jdbc、spring-orm)、事务管理(spring-tx)、Web(spring-web)等

<!-- Spring Core Container -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.3.10.RELEASE</version> <!-- 请替换为最新版本 -->
</dependency>

<!-- Spring AOP -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>5.3.10.RELEASE</version>
</dependency>

<!-- 其他Spring相关依赖... -->

然后需要手动创建Spring的配置文件(例如XML配置文件)来定义和配置Spring容器中的Bean、数据源、事务管理等。




    

如果你的项目是一个Web应用,你需要手动配置Servlet、Filter、Listener等。

<!-- web.xml -->
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
         version="3.1">

    <!-- 配置Servlet、Filter、Listener等... -->

</web-app>

最最麻烦的是,当项目比较复杂,要引入很多第三方组件时,XML中要配置的Bean也很多,而且有些Bean还有依赖关系,这就要求Bean配置的顺序不是随意的,而是有先后的。这对于项目维护是灾难。那SpringBoot是怎么解决这些问题的

组件扫描

SpringBoot中,不需要再配置XML文件来指定要注入那些Bean了,取而代之的是注解。那么在服务启动时,要把我们定义的组件扫描并加载到IOC容器中,那么就需要配置扫描路径。但是我们在启动SpringBoot项目时并没有配置扫描路径,它是怎么扫描到Bean然后注入到容器中的呢。

在SpringBoot中有两种类型的类路径需要扫描,一是项目根路径下的类,二是第三方jar包路径下的类。如下图

SpringBoot默认会扫描启动类下的包和子包中定义的Bean,例如被 @Controller,@Service,@Configuration注解的类,并加载到IOC容器中,在SpringBoot启动流程中,SpringApplication构造函数中,就获取了当前启动类的路径。

但是第三方的jar包,SpringBoot是怎么扫描到的呢,比如我们依赖了 spring-boot-starter-data-redis ,由于这个是第三方的 jar包,SpringBoot显然是不可能提前知道 redis相关 Bean的类路径的,而且我们也没有配置任何xml文件或是其它配置来告诉SpringBoot第三方 redis的类路径。来看看SpringBoot是怎么做到的

假设我们自己项目中开发了一个公共模块,这个模块是给其他业务系统使用的,比如编写了一个Robot用来监控应用是否健康。我们通常会有一些业务类。然后新建一个RobotAutoConfiguration类

@Import({RobotService.class, User.class, RobotController.class})
@Configuration
public class RobotAutoConfiguration {
}

然后我们在启动类中显示的引入这个配置类,就可以了

@SpringBootApplication
@Import(RobotAutoConfiguration.class)
public class StartApplication {

    public static void main(String[] args) {
        SpringApplication.run(StartApplication.class, args);
    }
}

这样就达成了我们目的,使用者只需要一个@Import(RobotAutoConfiguration.class)就能正常使用我们的Starter了。但是我们看SpringBoot官方的第三方 starter时并不需要显示注入类,只需要pom中依赖 jar包,就可以实现自动注入

如何全自动注入?

这就需要SPI登场了,META-INF/spring.factories这个文件几乎在所有的自动装配的starter中都有,它是实现自动装配注入的核心文件。在这个文件中指定了要扫描的类路径,SpringBoot会读取这个文件中配置的类路径,加载并注入到IOC中。说白了,就是与前面提到的以注解的方式注入Bean,SpringBoot还支持以文件配置的方式注入Bean。


Author: 顺坚
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint polocy. If reproduced, please indicate source 顺坚 !
评论
 Previous
Reactor模式的理解 Reactor模式的理解
Reactor模式在服务端中被广泛使用,Reactor是一个设计模式。Reactor把服务端的请求处理分为几个部分处理,提高了CPU的利用率。同时Reactor需要和NIO一起使用,才能使效率最高。 Class Reactor: /**
2024-08-25
Next 
读书笔记之共识算法 读书笔记之共识算法
《白话区块链》读书笔记,记录个人理解。区块链是一个去中心化的系统,因此没有一个权威节点做决策。需要全网所有的节点达成共识,才算决策的完成。首先说两个定理FLP与CAP FLP定理叫FLP是因为提出该定理的论文是由Fischer、Lynch和
2024-04-21
  TOC