《SpringBoot启动流程二》:七千字源码分析SpringApplication构造阶段

简介: 《SpringBoot启动流程二》:七千字源码分析SpringApplication构造阶段

@[TOC]

一、前言

上文聊了 SpringBoot如何加载并处理spring.factories文件中的信息。本文我们接着聊在构建SpringApplication的过程中都做了什么?加载spring.factories文件中的哪些信息?

注:Spring Boot版本:2.3.7

二、初始化SpringApplication

在这里插入图片描述

先看一个简单的SpringBoot 启动类:

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

根据SpringApplication的实现,当前启动类StartApplication的引导语句SpringApplication.run(StartApplication.class, args);经过两层深入,会进入到new SpringApplication(primarySources).run(args)

在这里插入图片描述

这里需要Class类型的primarySources参数,下面我先看一下这个参数有什么实际意义?

1、primarySources参数

primarySources参数实际为Spring Boot应用上下文的Configuration Class。换言之,Spring Boot应用上下文的Configuration Class也不一定非得是引导类(启动类),我们可以把Configuration Class抽出来,使用@EnableAutoConfiguration注解标注,然后将引导类(启动类)中的primarySources参数设置为抽出来的Configuration Class

示例如下:

1> 自定义 Configuration Class:

@Configuration
@EnableAutoConfiguration
public class MySpringApplicationConfiguration {
}

2> 修改引导类(启动类):

  • 引导类中不再需要添加@SpringBootApplication注解
public class StartApplication {
    public static void main(String[] args) {
        SpringApplication.run(MySpringApplicationConfiguration.class, args);
}

2、SpringApplication构造过程

已知SpringApplication#run(Class, String...)方法的执行会伴随SpringApplication对象的初始化,其构造函数为:SpringApplication(Class...)
在这里插入图片描述
具体操作包括:

  1. 首先初始化资源加载器,默认为null;断言判断主要资源类不能为null,否者报错。
  2. 然后将主资源类primarySources存储到SpringApplication对象Set类型的primarySources属性中。
  3. 推断当前 WEB 应用类型,一共有三种:NONE,SERVLET,REACTIVE;默认是SERVLET。
  4. 加载Spring应用上下文初始化器:从"META-INF/spring.factories"文件中读取ApplicationContextInitializer类的实例名称集合,然后进行Set去重、利用反射实例化对象,最后按照Order排序后,赋值到SpringApplication的List类型的initializers属性上,一共7个。
  5. 加载Spring应用事件监听器:从"META-INF/spring.factories"文件中读取ApplicationListener类的实例名称集合,然后进行Set去重、利用反射实例化对象,最后按照Order排序后,赋值到SpringApplication的List类型的listeners属性上,一共11个。
  6. 推断主入口应用类:通过当前调用栈的解析,获取Main方法所在类,并赋值给SpringApplication的mainApplicationClass属性。
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
        // 初始化资源加载器,默认为null
        this.resourceLoader = resourceLoader;
        // 断言 判断主要资源类不能为 null,否则报错
        Assert.notNull(primarySources, "PrimarySources must not be null");
        // 初始化主要资源类集合并去重
        this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
        // 推断当前 WEB 应用类型,一共有三种:NONE,SERVLET,REACTIVE
        this.webApplicationType = WebApplicationType.deduceFromClasspath();
        // 加载并设置Spring应用上下文初始化器, 从"META-INF/spring.factories"读取ApplicationContextInitializer类的实例名称集合,
        // 并进行set去重,然后利用反射实例化对象,最后进行Order排序。(一共7个)
        setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
        // 加载并设置Spring应用事件监听器,从"META-INF/spring.factories"读取ApplicationListener类的实例名称集合,
        // 并进行set去重,然后利用反射实例化对象,最后进行Order排序。(一共11个)
        setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
        // 推断主应用程序类,通过当前调用栈,获取Main方法所在类,并赋值给mainApplicationClass
        this.mainApplicationClass = deduceMainApplicationClass();
    }

下面具体看一下是怎么推断Web应用类型的?怎么加载Spring应用上下文初始化器、怎么加载Spring应用事件监听器的?又是怎么推断应用引导类的?

1)推断Web应用类型

this.webApplicationType = WebApplicationType.deduceFromClasspath();

因为Web应用类型可能在SpringApplication构造后及run方法之前,再通过setWebApplicatioinType(WebApplicationType)方法调整;又在推断Web应用类型的过程中,由于当前Spring应用上下文尚未准备,所以采用检查当前ClassLoader下基准Class的存在性来推断Web应用类型。

public enum WebApplicationType {

    NONE,
    SERVLET,
    REACTIVE;

    private static final String[] SERVLET_INDICATOR_CLASSES = { "javax.servlet.Servlet",
            "org.springframework.web.context.ConfigurableWebApplicationContext" };

    private static final String WEBMVC_INDICATOR_CLASS = "org.springframework.web.servlet.DispatcherServlet";

    private static final String WEBFLUX_INDICATOR_CLASS = "org.springframework.web.reactive.DispatcherHandler";

    private static final String JERSEY_INDICATOR_CLASS = "org.glassfish.jersey.servlet.ServletContainer";
    
    ....

    static WebApplicationType deduceFromClasspath() {
        // 1. 如果`DispatcherHandler`存在,并且`DispatcherServlet`和`ServletContainer`不存在时,Web应用类型为REACTIVE;
        if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null) && !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null)
                && !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) {
            return WebApplicationType.REACTIVE;
        }
        // 2. 如果`Servlet`和`ConfigurableWebApplicationContext`不存在,则当前应用为非Web引应用,即NONE。
        for (String className : SERVLET_INDICATOR_CLASSES) {
            if (!ClassUtils.isPresent(className, null)) {
                return WebApplicationType.NONE;
            }
        }
        // 3.当Spring WebFlux和Spring Web MVC同时存在时,Web应用依旧是SERVLET。
        return WebApplicationType.SERVLET;
    }
    }
    ....
}

WEB 应用类型,一共有三种:NONE,SERVLET,REACTIVE。

deduceFromClasspath()方法利用ClassUtils.isPresent(String, ClassLoader)方法依次判断reactive.DispatcherHandlerConfigurableWebApplicationContextServletservlet.DispatcherServlet的存在性组合情况,从而判断Web 引用类型,具体逻辑如下:

  1. 如果DispatcherHandler存在,并且DispatcherServletServletContainer不存在时,即:Spring Boot仅依赖WebFlux时,Web应用类型为REACTIVE;
  2. 如果ServletConfigurableWebApplicationContext不存在,则当前应用为非Web应用,即NONE。因为这两个API是Spring Web MVC必须的依赖。
  3. 当Spring WebFlux和Spring Web MVC同时存在时,Web应用类型依旧是SERVLET。

deduceFromClasspath()方法执行完毕后,SpringApplication的构造进入“加载Spring应用上下文初始化器、Spring事件监听器”过程。

2)加载Spring应用上下文初始化器ApplicationContextInitializer

setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));

这个过程包括两个动作:

1> getSpringFactoriesInstances(ApplicationContextInitializer.class)从"META-INF/spring.factories"文件中读取 ApplicationContextInitializer类的实例名称集合,然后进行Set去重、利用反射实例化对象,最后按照Order排序。
2> setInitializers(Collection)将Collection赋值到SpringApplication的List类型的initializers属性上,一共7个。

getSpringFactoriesInstances(Class)在前一篇博文中详细聊过,移步:《SpringBoot启动流程一》:万字debug梳理SpringBoot如何加载并处理META-INF/spring.factories文件中的信息

第二步单纯的赋值:

public void setInitializers(Collection<? extends ApplicationContextInitializer<?>> initializers) {
    this.initializers = new ArrayList<>(initializers);
}

3)加载Spring事件应用监听器ApplicationListener

setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));

这个过程和加载Spring应用上下文初始化器ApplicationContextInitializer一样(区别在于这里不再直接从spring.factories文件中获取内容,而是走cache(MultiValueMap<String, String>)缓存),也是包括两个动作(第一个动作一样)。

第二步 setListeners(Collection)将Collection赋值到SpringApplication的List类型的listeners属性上,一共11个。

public void setListeners(Collection<? extends ApplicationListener<?>> listeners) {
    this.listeners = new ArrayList<>(listeners);
}

加载结果如下:
在这里插入图片描述

4)推断应用引导类

this.mainApplicationClass = deduceMainApplicationClass();

推断应用引导类是SpringApplication构建过程的最后一步,其执行方法deduceMainApplicationClass()

private Class<?> deduceMainApplicationClass() {
    try {
        StackTraceElement[] stackTrace = new RuntimeException().getStackTrace();
        for (StackTraceElement stackTraceElement : stackTrace) {
            if ("main".equals(stackTraceElement.getMethodName())) {
                return Class.forName(stackTraceElement.getClassName());
            }
        }
    }
    catch (ClassNotFoundException ex) {
        // Swallow and continue
    }
    return null;
}

该方法根据当前线程执行栈来判断其栈中哪个类包含main方法,然后将找到的类名通过反射返回Class对象。

在这里插入图片描述

至此,在SpringApplication构造过程中,SpringApplication属性primarySources、webApplicationType、initializers、listeners 和 mainApplicationClass都被初始化了,下一篇文博文继续聊SpringApplication的准备阶段。即:从run(String...)方法调用开始 到 refreshContext(ConfigurableApplicationContext调用前)

相关文章
|
7月前
|
安全 Java Spring
SpringBoot2 | SpringBoot监听器源码分析 | 自定义ApplicationListener(六)
SpringBoot2 | SpringBoot监听器源码分析 | 自定义ApplicationListener(六)
102 0
|
7月前
|
设计模式 Java 容器
SpringBoot2 | SpringBoot启动流程源码分析(二)
SpringBoot2 | SpringBoot启动流程源码分析(二)
85 0
|
7月前
|
前端开发 Java Spring
SpringBoot2 | SpringBoot Environment源码分析(四)
SpringBoot2 | SpringBoot Environment源码分析(四)
73 0
|
7月前
|
Java 中间件 容器
SpringBoot2 | SpringBoot启动流程源码分析(一)
SpringBoot2 | SpringBoot启动流程源码分析(一)
91 0
|
7月前
|
XML Java 应用服务中间件
SpringBoot配置外部Tomcat项目启动流程源码分析(长文)
SpringBoot配置外部Tomcat项目启动流程源码分析(长文)
594 0
|
Java 应用服务中间件 数据库连接
头秃系列,二十三张图带你从源码分析Spring Boot 启动流程~
前言 源码版本 从哪入手? 源码如何切分? 如何创建SpringApplication? 设置应用类型 设置初始化器(Initializer) 设置监听器(Listener) 设置监听器(Listener) 执行run()方法 获取、启动运行过程监听器 环境构建 创建IOC容器 IOC容器的前置处理 刷新容器 IOC容器的后置处理 发出结束执行的事件 执行Runners 总结 总结
|
Java 测试技术 容器
全网最详细的介绍SpringBoot启动过程源码分析
上一篇我们介绍了SpringBoot的自动装配的知识,这一篇我们将介绍SpringBoot最核心的知识点,SpringBoot应用的启动过程。这个启动过程比较复杂,在此我只介绍核心的知识点。其启动过程大概分为两步。1. 初始化SpringApplication对象,2.执行SpringApplication对象的run方法。
163 0
全网最详细的介绍SpringBoot启动过程源码分析
|
2月前
|
JavaScript 安全 Java
如何使用 Spring Boot 和 Ant Design Pro Vue 实现动态路由和菜单功能,快速搭建前后端分离的应用框架
本文介绍了如何使用 Spring Boot 和 Ant Design Pro Vue 实现动态路由和菜单功能,快速搭建前后端分离的应用框架。首先,确保开发环境已安装必要的工具,然后创建并配置 Spring Boot 项目,包括添加依赖和配置 Spring Security。接着,创建后端 API 和前端项目,配置动态路由和菜单。最后,运行项目并分享实践心得,包括版本兼容性、安全性、性能调优等方面。
190 1
|
1月前
|
JavaScript 安全 Java
如何使用 Spring Boot 和 Ant Design Pro Vue 构建一个具有动态路由和菜单功能的前后端分离应用。
本文介绍了如何使用 Spring Boot 和 Ant Design Pro Vue 构建一个具有动态路由和菜单功能的前后端分离应用。首先,创建并配置 Spring Boot 项目,实现后端 API;然后,使用 Ant Design Pro Vue 创建前端项目,配置动态路由和菜单。通过具体案例,展示了如何快速搭建高效、易维护的项目框架。
119 62
|
14天前
|
存储 JavaScript 前端开发
基于 SpringBoot 和 Vue 开发校园点餐订餐外卖跑腿Java源码
一个非常实用的校园外卖系统,基于 SpringBoot 和 Vue 的开发。这一系统源于黑马的外卖案例项目 经过站长的进一步改进和优化,提供了更丰富的功能和更高的可用性。 这个项目的架构设计非常有趣。虽然它采用了SpringBoot和Vue的组合,但并不是一个完全分离的项目。 前端视图通过JS的方式引入了Vue和Element UI,既能利用Vue的快速开发优势,
79 13