首页 > 其他分享 >技术分享:了解 Spring Boot 启动类 SpringApplication

技术分享:了解 Spring Boot 启动类 SpringApplication

时间:2024-07-03 18:19:47浏览次数:19  
标签:Spring Boot WebApplicationType SpringApplication primarySources 加载

在学习上述 Spring Boot 核心功能的过程中,相信大家可能都会尝试启动自己新建的 Spring Boot 的项目,
并 Debug 看看具体的执行过程。本篇开始就将从 Spring Boot 的启动类 SpringApplication 上入手,
带领大家了解 Spring Boot 启动过程中所涉及到的源码和知识点。
先来看看 run 方法的源码:

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);
}
阅读上述 run 方法,我们可以看到实际上是 new 了一个 SpringApplication 对象【其构造参数 primarySources 为加载的主要资源类,通常就是 SpringBoot 的入口类】,并调用其 run 方法【其参数 args 为传递给应用程序的参数信息】启动,然后返回一个应用上下文对象 ConfigurableApplicationContext 。

通过观察这个内部的 run 方法实现,我们也可以在自己的 Spring Boot 启动入口类中,像如下这样去写 :

@SpringBootApplication
public class DemoApplication {

public static void main(String[] args) {
    SpringApplication springApplication = new SpringApplication(DemoApplication.class);
    // 这里可以调用 SpringApplication 提供的 setXX 或 addXX 方法来定制化设置
    springApplication.run(args);
}

}
2. SpringApplication 的实例化
上面已经看到我们在实例化 SpringApplication 了,废话不多说,直接翻看其源码【Spring Boot 2.7.9】:

public SpringApplication(Class<?>... primarySources) {
this(null, primarySources);
}

@SuppressWarnings({ "unchecked", "rawtypes" })
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
    this.resourceLoader = resourceLoader;
    Assert.notNull(primarySources, "PrimarySources must not be null");
    this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
    // 推断web应用类型
    this.webApplicationType = WebApplicationType.deduceFromClasspath();
    // 加载并初始化 BootstrapRegistryInitializer及其实现类
    this.bootstrapRegistryInitializers = new ArrayList<>(
            getSpringFactoriesInstances(BootstrapRegistryInitializer.class));
    // 加载并初始化 ApplicationContextInitializer及其实现类
    setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
    // 加载并初始化ApplicationListener及其实现类
    setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
    // 推断入口类
    this.mainApplicationClass = deduceMainApplicationClass();
}

由上可知,SpringApplication 提供了两个构造方法,而其核心的逻辑都在第二个构造方法中实现。

2.1 构造方法参数
我们从上述源码可知,SpringApplication 的第二个构造方法有两个参数,分别是:

ResourceLoader resourceLoader :ResourceLoader 为资源加载的接口,它用于在 Spring Boot 启动时打印对应的 banner 信息,默认采用的就是 DefaultResourceLoader。实操过程中,如果未按照 Spring Boot 的 “约定” 将 banner 的内容放置于 classpath 下,或者文件名不是 banner.* 格式,默认资源加载器是无法加载到对应的 banner 信息的,此时则可通过 ResourceLoader 来指定需要加载的文件路径【这个后面我们专门来实操一下,敬请期待】。

Class<?>... primarySources :主要的 bean 来源,该参数为可变参数,默认我们会传入 Spring Boot 的入口类【即 main 方法所在的类】,如上面我们的 DemoApplication 。如果作为项目的引导类,该类需要满足一个条件,就是被注解 @EnableAutoConfiguration 或其组合注解标注。在前面的《【Spring Boot 源码学习】@SpringBootApplication 注解》博文中,我们已经知道 @SpringBootApplication 注解中包含了 @EnableAutoConfiguration 注解,因此被 @SpringBootApplication 注解标注的类也可作为参数传入。当然,primarySources 也可传入其他普通类,但只有传入被 @EnableAutoConfiguration 标注的类才能够开启 Spring Boot 的自动配置。

有些朋友,可能对 primarySources 这个可变参数的描述有点疑惑,下面我们就用实例来演示以其他引导类为入口类进行 Spring Boot 项目启动:

首先,我们在入口类 DemoApplication 的同级目录创建一个 SecondApplication 类,使用 @SpringBootApplication 进行注解。

@SpringBootApplication
public class SecondApplication {
}
然后,将 DemoApplication 修改成如下:

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

最后,我们来运行 DemoApplication 的 main 方法。

从上图可以看出,我们的应用依然能正常启动,并完成自动配置。因此,决定 Spring Boot 启动的入口类并不是一定是 main 方法所在类,而是直接或间接被 @EnableAutoConfiguration 标注的类。

翻看 SpringApplication 的源码,我们在其中还能看到它提供了追加 primarySources 的方法,如下所示:

public void addPrimarySources(Collection<Class<?>> additionalPrimarySources) {
this.primarySources.addAll(additionalPrimarySources);
}
如果采用 1 中最后的方式启动 Spring Boot ,我们就可以调用 addPrimarySources 方法来追加额外的 primarySources。

我们继续回到 SpringApplication 的构造方法里,可以看到如下的代码:

this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
上述这里将 primarySources 参数转换为 LinkedHashSet 集合,并赋值给 SpringApplication 的私有成员变量 Set<Class<?>> primarySources。

知识点: LinkedHashSet 是 Java 集合框架中的类,它继承自 HashSet,因此具有哈希表的查找性能。这是一个同时使用链表和哈希表特性的数据结构,其中链表用于维护元素的插入顺序。也即是说,当你向 LinkedHashSet 添加元素时,元素将按照添加的顺序被存储,并且能够被遍历输出。

此外,LinkedHashSet 还确保了 元素的唯一性,即重复的元素在集合中只会存在一份。

如果需要频繁遍历集合,那么 LinkedHashSet 可能会比 HashSet 效率更高,因为其通过维护一个双向链表来记录元素的添加顺序,从而支持按照插入顺序排序的迭代。但需要注意的是,LinkedHashSet 是非线程安全的,如果有多个线程同时访问该集合容器,可能会引发并发问题。

2.2 Web 应用类型推断
我们继续往下翻看源码,这里调用了 WebApplicationType 的 deduceFromClasspath 方法来进行 Web 应用类型的推断。

this.webApplicationType = WebApplicationType.deduceFromClasspath();
我们继续翻看 WebApplicationType 的源码:

public enum WebApplicationType {
// 非Web应用类型
NONE,
// 基于Servlet的Web应用类型
SERVLET,
// 基于reactive的Web应用类型
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() {
    if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null) && !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null)
            && !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) {
        return WebApplicationType.REACTIVE;
    }
    for (String className : SERVLET_INDICATOR_CLASSES) {
        if (!ClassUtils.isPresent(className, null)) {
            return WebApplicationType.NONE;
        }
    }
    return WebApplicationType.SERVLET;
}

}
WebApplicationType 是一个定义了可能的 Web 应用类型的枚举类,该枚举类中包含了三块逻辑:

枚举类型 :非 Web 应用、基于 Servlet 的 Web 应用和基于 reactive 的 Web 应用。

用于下面推断的常量

推断类型的方法 deduceFromClasspath :

当 DispatcherHandler 存在,并且 DispatcherServlet 和 ServletContainer 都不存在,则返回类型为 WebApplicationType.REACTIVE。

当 Servlet 或 ConfigurableWebApplicationContext 任何一个不存在时,则说明当前应用为非 Web 应用,返回 WebApplicationType.NONE。

当应用不为 reactive Web 应用,并且 Servlet 和 ConfigurableWebApplicationContext 都存在的情况下,则返回 WebApplicationType.SERVLET。

在上述的 deduceFromClasspath 方法中,我们可以看到,在判断的过程中使用到了 ClassUtils 的 isPresent 方法。该工具类方法就是通过反射创建指定的类,根据在创建过程中是否抛出异常来判断该类是否存在。

2.3 加载 BootstrapRegistryInitializer
this.bootstrapRegistryInitializers = new ArrayList<>(
getSpringFactoriesInstances(BootstrapRegistryInitializer.class));
上述逻辑用于加载并初始化 BootstrapRegistryInitializer 及其相关的类。

BootstrapRegistryInitializer 是 Spring Cloud Config 的组件之一,它的作用是在应用程序启动时初始化 Spring Cloud Config 客户端。

在 Spring Cloud Config 中,客户端通过向配置中心(Config Server)发送请求来获取应用程序的配置信息。而 BootstrapRegistryInitializer 就是负责将配置中心的相关信息注册到 Spring 容器中的。

由于篇幅有限,有关 BootstrapRegistryInitializer 更详细的内容,笔者后续专门讲解。

2.4 加载 ApplicationContextInitializer
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
上述代码用于加载并初始化 ApplicationContextInitializer 及其相关的类。

ApplicationContextInitializer 是 Spring 框架中的一个接口,它的主要作用是在 Spring 容器刷新之前初始化 ConfigurableApplicationContext。这个接口的实现类可以被视为回调函数,它们的 onApplicationEvent 方法会在 Spring 容器启动时被自动调用,从而允许开发人员在容器刷新之前执行一些自定义的操作。

例如,我们可能需要在这个时刻加载一些配置信息,或者对某些 bean 进行预处理等。通过实现 ApplicationContextInitializer 接口并重写其 onApplicationEvent 方法,就可以完成这些定制化的需求。

由于篇幅有限,有关 ApplicationContextInitializer 更详细的内容,笔者后续专门讲解。

2.5 加载 ApplicationListener
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
上述代码用于加载并初始化 ApplicationListener 及其相关的类。

ApplicationListener 是 Spring 框架提供的一个事件监听机制,它是 Spring 应用内部的事件驱动机制,通常被用于监控应用内部的运行状况。其实现的原理是 观察者设计模式,该设计模式的初衷是为了实现系统业务逻辑之间的解耦,从而提升系统的可扩展性和可维护性。

我们可以通过自定义一个类来实现 ApplicationListener 接口,然后在这个类中定义需要监听的事件处理方法。当被监听的事件发生时,Spring 会自动调用这个方法来处理事件。例如,在一个 Spring Boot 项目中,我们可能想要在容器启动时执行一些特定的操作,如加载配置等,就可以通过实现 ApplicationListener 接口来完成。

由于篇幅有限,有关 ApplicationListener 更详细的内容,笔者后续专门讲解。

2.6 推断应用入口类
最后一步,调用 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) {
// 这里捕获异常,并继续执行后续逻辑
}
return null;
}
上述代码的思路就是:

首先,创建一个运行时异常,并获得其堆栈数组。
接着,遍历数组,判断类的方法中是否包含 main 方法。第一个被匹配的类会通过 Class.forName 方法创建对象,并将其被返回。
最后,将上述创建的 Class 对象赋值给 SpringApplication 的成员变量 mainApplicationClass。
总结
本篇 Huazie 带大家初步了解了 SpringApplication 的实例化过程,当然由于篇幅受限,还有些内容暂时无法详解,Huazie 将在后续的博文中继续深入分析。

只有了解 Spring Boot 在启动时都做了些什么,我们才能在后续的实践的过程中更好地理解其运行机制,以便遇到问题能更快地定位和排查,使我们应用能够更容易、更方便地接入 Spring Boot 。

标签:Spring,Boot,WebApplicationType,SpringApplication,primarySources,加载
From: https://www.cnblogs.com/pony100/p/18282349

相关文章

  • springboot实验报告管理系统-计算机毕业设计源码10596
    目录1绪论1.1选题背景与意义1.2国内外研究现状1.3论文结构与章节安排2系统分析2.1可行性分析2.2系统流程分析2.2.1系统开发流程2.2.2用户登录流程2.2.3系统操作流程2.2.4添加信息流程2.2.5修改信息流程2.2.6删除信息流程2.3 系统功能分......
  • springboot实验报告管理系统-计算机毕业设计源码10596
    目录1绪论1.1选题背景与意义1.2国内外研究现状1.3论文结构与章节安排2系统分析2.1可行性分析2.2系统流程分析2.2.1系统开发流程2.2.2用户登录流程2.2.3系统操作流程2.2.4添加信息流程2.2.5修改信息流程2.2.6删除信息流程2.3 系统功能分......
  • springboot线程池简单配置
    @ConfigurationpublicclassThirdPartyOilSmallTaskConfig{@Bean("thirdPartyOilSmallTaskExecutor")publicThreadPoolTaskExecutoruniteOilThreadPoolTaskExecutor(){ThreadPoolTaskExecutorexecutor=newThreadPoolTaskExecutor();......
  • springboot集成minIO
    文件系统:负责管理和存储文件的系统软件。操作系统通过文件系统提供的接口去存取文件,用户通过操作系统访问磁盘上的文件minIO:轻量级服务分布式文件系统,适合存储非机构化数据。采用去中心化共享架构,结点之间是对等关系。 端口号为9000,初次登录账号密码都是minioadmin minIO中......
  • 微服务数据流的协同:Eureka与Spring Cloud Data Flow集成指南
    微服务数据流的协同:Eureka与SpringCloudDataFlow集成指南在构建基于SpringCloud的微服务架构时,服务发现和数据流处理是两个关键的组成部分。Eureka作为服务发现工具,而SpringCloudDataFlow提供了数据流处理的能力。本文将详细介绍如何将Eureka与SpringCloudDataFl......
  • Springboot+Vue加密通信
    前言本文旨在给出Springboot+Vue框架下的加密通信具体实现,同时为照顾非行业内/初学读者,第一小节浅显的解释下加解密方式,老鸟直接跳过。1加解密方式常见的加解密方式大概分成对称加密、非对称加密与信息摘要算法三类。下面仅从使用角度简单介绍下加解密方式:1.1对称......
  • spring-security安全框架(超精细版附带流程讲解图)
    目录一、回顾一下二、security使用2.1覆盖掉默认配置「自定义配置」2.2如何自定义认证2.3纯纯自定义2.4jwt2.5官网认证流程2.6 RBAC模型4.1.创建表结构2.7如何实现权限流程一、回顾一下security干啥的?认证和授权使用方式引入依赖,基于springboo......
  • springboot使用注解方式打印方法日志
    springboot使用注解方式打印方法日志,可以很方便的打印日志,通用性很强。耦合很低,很好。作为程序员的我不废话,咱们直接上代码先创建个注解@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)@Documentedpublic@interfaceShowLog{/***日志描......
  • Spring的三种依赖注入的方式
    1、什么是依赖注入依赖注入(DependencyInjection,简称DI),是IOC的一种别称,用来减少对象间的依赖关系。提起依赖注入,就少不了IOC。IOC(InversionofControl,控制反转)是一种设计思想,它将原本在程序中手动创建对象的控制权,交由Spring框架来管理。IOC和DI,是同一个概念的不同角度描述。......
  • springboot的MultipartFile转File读取
    在SpringBoot中,处理文件上传时,MultipartFile接口被用来封装上传的文件信息。如果需要将MultipartFile转换为Java标准的File对象进行读取。以下是具体的操作流程:1.创建临时文件        首先,需要将接收到的MultipartFile对象转换为一个临时File对象。      ......