SpringBoot源码解读与原理分析(十四)SpringApplication的总体设计

文章目录

  • 4 SpringBoot的核心引导:SpringApplication
    • 4.1 总体设计
      • 4.1.1 启动失败的错误报告
      • 4.1.2 Bean的延迟初始化
      • 4.1.3 SpringApplication的定制
      • 4.1.4 Web类型推断
      • 4.1.5 监听与回调
        • 1.核心监听类SpringApplicationRunListener
        • 2.SpringBoot新引入的事件
      • 4.1.6 应用退出

4 SpringBoot的核心引导:SpringApplication

4.1 总体设计

这篇文章主要了解SpringApplication的设计,其主要内容来源是javadoc和官方文档。

在SpringBoot官方文档中Core Features部分,第1大点就整体介绍了SpringApplication。

The SpringApplicationclass provides a convenient way to bootstrap a Spring application that is started from a main()method. In many situations, you can delegate to the static SpringApplication.runmethod, as shown in the following example:

@SpringBootApplication
public class MyApplication {

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

}

SpringApplication类提供了一个方便的方式,利用main()方法来引导SpringBoot应用程序。大多数情况下,启动SpringBoot应用程序只需要委托给静态的SpringApplication.run方法。

可见,SpringApplication的核心作用是简化SpringBoot应用程序的启动。官方文档的后续内容,针对SpringApplication背后非常多的特性进行了详细的解释,这里挑选一些比较重要的特性来讨论下。

4.1.1 启动失败的错误报告

当编写的SpringBoot应用程序启动失败时,控制台会打印本次启动失败的信息(例如Bean不存在、端口被占用等)。下面是8080端口被占用的错误报告:

***************************
APPLICATION FAILED TO START
***************************

Description:

Web server failed to start. Port 8080 was already in use.

Action:

Identify and stop the process that's listening on port 8080 or configure this application to listen on another port.

负责输出错误报告的是一个名为FailureAnalyzers的组件,顾名思义就是负责错误分析的组件,该组件实现了SpringBootExceptionReporter接口。

final class FailureAnalyzers implements SpringBootExceptionReporter {
    private final List<FailureAnalyzer> analyzers;
    
    FailureAnalyzers(ConfigurableApplicationContext context, ClassLoader classLoader) {
        // ...
        this.analyzers = loadFailureAnalyzers(this.classLoader);
        prepareFailureAnalyzers(this.analyzers, context);
    }
    
    private List<FailureAnalyzer> loadFailureAnalyzers(ClassLoader classLoader) {
        List<String> analyzerNames = SpringFactoriesLoader.loadFactoryNames(FailureAnalyzer.class, classLoader);
        List<FailureAnalyzer> analyzers = new ArrayList<>();
        // ...
    }
    
}

由源码可知,该组件内部集成了一组FailureAnalyzer,集成方式是利用SpringFramework的SPI机制,即从META-INF/spring.factories中读取所有的FailureAnalyzer。在spring-boot依赖的spring.factories中对FailureAnalyzer进行了配置:

spring-boot-autoconfigure-2.3.11.RELEASE.jar!/META-INF/spring.factories

# Failure analyzers
org.springframework.boot.diagnostics.FailureAnalyzer=\
org.springframework.boot.autoconfigure.data.redis.RedisUrlSyntaxFailureAnalyzer,\
org.springframework.boot.autoconfigure.diagnostics.analyzer.NoSuchBeanDefinitionFailureAnalyzer,\
org.springframework.boot.autoconfigure.flyway.FlywayMigrationScriptMissingFailureAnalyzer,\
org.springframework.boot.autoconfigure.jdbc.DataSourceBeanCreationFailureAnalyzer,\
org.springframework.boot.autoconfigure.jdbc.HikariDriverConfigurationFailureAnalyzer,\
org.springframework.boot.autoconfigure.r2dbc.ConnectionFactoryBeanCreationFailureAnalyzer,\
org.springframework.boot.autoconfigure.session.NonUniqueSessionRepositoryFailureAnalyzer

由该配置文件可知,SpringBoot默认情况下已经检查了一些常见的异常类型。如果实际开发中发现默认检查的类型不够完善,可以自行编写FailureAnalyzer的实现类,自行解析异常和输出。实际上,也可以通过调整日志输出级别为DEBUG来跟踪错误。

4.1.2 Bean的延迟初始化

官方文档描述了Bean的延迟初始化的作用及其局限性。

SpringApplicationallows an application to be initialized lazily. When lazy initialization is enabled, beans are created as they are needed rather than during application startup. As a result, enabling lazy initialization can reduce the time that it takes your application to start. In a web application, enabling lazy initialization will result in many web-related beans not being initialized until an HTTP request is received.

SpringApplication允许延迟初始化应用程序。当启用延迟初始化时,bean会在需要时才创建,而不是在应用程序启动时就创建。因此,启用延迟初始化可以减少应用程序启动所需的时间。在Web应用程序中,启用延迟初始化会导致许多与Web相关的bean收到HTTP请求后才初始化。

A downside of lazy initialization is that it can delay the discovery of a problem with the application. If a misconfigured bean is initialized lazily, a failure will no longer occur during startup and the problem will only become apparent when the bean is initialized. Care must also be taken to ensure that the JVM has sufficient memory to accommodate all of the application’s beans and not just those that are initialized during startup. For these reasons, lazy initialization is not enabled by default and it is recommended that fine-tuning of the JVM’s heap size is done before enabling lazy initialization.

延迟初始化的一个缺点是,它会延迟发现应用程序的问题。如果被错误配置的bean是延迟初始化的,那么在启动过程中就不会报错,只有在该bean初始化时,问题才会出现。还必须注意,要确保JVM有足够的内存容纳应用程序的所有bean,而不仅仅是那些在启动期间初始化的bean。出于这些原因,默认情况下没有启用延迟初始化,建议在启用延迟初始化之前对JVM的堆大小进行微调。

启用延迟初始化,可以通过编程的方式(SpringApplicationBuilder类的lazyInitialization方法或SpringApplication类的setLazyInitialization方法)或配置文件方式:

spring.main.lazy-initialization=true

如果在全局开启了Bean的延迟初始化,但是需要某些Bean对象在应用程序启动时就初始化,可以在类上标注@Lazy(false)注解。

4.1.3 SpringApplication的定制

通常情况下,在编写SpringBoot应用程序的主启动类时,会直接使用SpringApplication.run来实现。但也可以使用SpringApplication的API来定制化启动SpringBoot应用程序。

  • 方式一:直接创建SpringApplication对象并调用其API
public static void main(String[] args) {
    // 直接创建SpringApplication对象
    SpringApplication springApplication = new SpringApplication();
    // 设置主启动类
    springApplication.setMainApplicationClass(MyApp.class);
    // 关闭Banner打印
    springApplication.setBannerMode(Banner.Mode.OFF);
    springApplication.run(args);
}
  • 方式二:借助SpringApplicationBuilder实现链式定制
public static void main(String[] args) {
    new SpringApplicationBuilder(MyApp.class)
            .bannerMode(Banner.Mode.OFF)
            .web(WebApplicationType.NONE) //指定Web应用类型为非Web
            .run(args);
}

利用SpringApplicationBuilder还可以构建具有层次关系的SpringBoot应用程序:

new SpringApplicationBuilder().sources(Parent.class)
    .child(Application.class)
    .bannerMode(Banner.Mode.OFF)
    .run(args);

这种层次关系会使应用程序内部形成多个ApplicationContext且彼此之间是父子关系。但这种层次结构有一定的限制:Web组件必须包含在子上下文中,并且Environment对于父上下文和子上下文都相同。

4.1.4 Web类型推断

SpringBoot的一大优势就是整合不同场景时简单易操作。当项目需要整合SpringWebMvc时,只需引入spring-boot-starter-web依赖,整合SpringWebFlux时,只需引入spring-boot-starter-webflux依赖,SpringBoot会在底层推断应该应用哪种IOC容器。

SpringBoot 2.x基于SpringFramework 5.x, Web场景的解决方案有WebMvc和WebFlux两种,所以在SpringBoot中对于Web应用的类型也相应分为Servlet(WebMvc)、Reactive(WebFlux)和None三种。

根据官方文档,Web类型推断的规则如下:

  • 如果SpringWebMvc存在,则启动Servlet环境;
  • 如果SpringWebMvc不存在并且SpringWebFlux存在,则启用Reactive环境;
  • 如果两者都不存在,则启用最原始的没有任何Web概念的None环境。
  • 如果SpringWebMvc和SpringWebFlux同时存在,则默认SpringWebFlux失效,除非手动定制SpringApplication指定WebApplicationType为Reactive。

4.1.5 监听与回调

SpringFramework已经构建了一套强大且完善的事件监听机制,开发者可以在基于SpringFramework的应用程序中任意编写事件,实现自定义的事件监听。而且SpringFramework已经预先构建了一些基于ApplicationContext的事件(如ContextRefreshedEvent)。

SpringBoot将事件监听机制进一步扩展。在执行SpringApplication.run时,也有生命周期,SpringBoot在该生命周期中扩展了新的切入点,也就是基于SpringApplication的事件监听,其核心监听类是SpringApplicationRunListener,专门用于广播SpringBoot事件。

1.核心监听类SpringApplicationRunListener

官方文档没有关于SpringApplicationRunListener的内容,只在其javadoc中可以看到少量描述:

Listener for the SpringApplication.runmethod. SpringApplicationRunListeners are loaded via the SpringFactoriesLoaderand should declare a public constructor that accepts a SpringApplicationinstance and a String[]of arguments. A new SpringApplicationRunListenerinstance will be created for each run.

这是SpringApplication.run方法的监听器。SpringApplicationRunListener是通过SpringFactoriesLoader加载的,并且应该声明一个公共构造函数,该构造函数接受一个SpringApplication实例和一个String[]参数。每次运行都会创建一个新的SpringApplicationRunListener实例。

SpringApplicationRunListener本身是一个接口,定义了7个事件回调的方法:

接口方法 可获得的组件 回调时机
starting 调用SpringApplication.run方法是立即调用
environmentPrepared ConfigurableEnvironment Environment构建完成,但在创建ApplicationContext之前
contextPrepared ConfigurableApplicationContext 在创建和准备ApplicationContext之后,但在加载之前
contextLoaded ConfigurableApplicationContext ApplicationContext已加载,但尚未刷新容器时
started ConfigurableApplicationContext IOC容器已刷新,但未调用CommandLineRunners和ApplicationRunners时
running ConfigurableApplicationContext 在run方法彻底完成之前
failed ConfigurableApplicationContext,Throwable run方法执行过程中抛出异常时

如果需要自定义SpringApplicationRunListener的实现类,要注意以下两点:

  • 在注册时不能直接使用@Component、@Bean等常规的Bean注册方式,而是需要配置到spring.factories文件中,利用SPI机制加载。内容如下:
org.springframework.context.ApplicationListener=com.example.project.MyListener

为什么要这么做呢?因为部分事件在广播时,ApplicationContext还没有被创建出来,自然也就无法有效地初始化监听器。

  • 必须显式声明一个公共构造函数,该构造函数接受一个SpringApplication实例和一个String[]参数。
public class TestRunListener implements SpringApplicationRunListener {
    public TestRunListener(SpringApplication application, String[] args) {}
}
2.SpringBoot新引入的事件

与SpringApplicationRunListener的各个事件方法相对应,SpringBoot给每个方法都定义了一个新的事件模型。对应关系如下:

接口方法 事件模型
starting ApplicationStartingEvent
environmentPrepared ApplicationEnvironmentPreparedEvent
contextPrepared ApplicationContextInitializedEvent
contextLoaded ApplicationPreparedEvent
started ApplicationStartedEvent
running ApplicationReadyEvent
failed ApplicationFailedEvent

4.1.6 应用退出

当SpringBoot启动成功后,内部的ApplicationContext会注册一个shutduwnhook线程。当JVM退出时,shutduwnhook线程可以确保IOC容器中的Bean能被IOC容器销毁阶段得生命周期进行回调(如被@PreDestroy注解标注的方法),从而合理地销毁IOC容器及其所有的Bean。

从源码角度,该关闭钩子的名称是固定的:

ConfigurableApplicationContext.java

String SHUTDOWN_HOOK_THREAD_NAME = "SpringContextShutdownHook";

线程中的执行逻辑就是调用doClose方法,该方法会销毁IOC容器中所有Bean,关闭BeanFactory。

AbstractApplicationContext.java

public void registerShutdownHook() {
    if (this.shutdownHook == null) {
        // No shutdown hook registered yet.
        // 创建钩子
        this.shutdownHook = new Thread(SHUTDOWN_HOOK_THREAD_NAME) {
            @Override
            public void run() {
                synchronized (startupShutdownMonitor) {
                    // 线程中调用销毁IOC容器的方法
                    doClose();
                }
            }
        };
        // 注册钩子
        Runtime.getRuntime().addShutdownHook(this.shutdownHook);
    }
}

更多内容请查阅分类专栏:SpringBoot源码解读与原理分析

下一篇的主要内容是:SpringApplication的启动阶段引导流程。

你可能感兴趣的:(spring,boot,java,后端,spring)