一文了解Spring Boot启动类SpringApplication

华为云开发者社区 2024-07-03 15:09:00 阅读 86

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

本文分享自华为云社区《【Spring Boot 源码学习】初识 SpringApplication》,作者: Huazie。

引言

往期的博文,Huazie围绕Spring Boot的核心功能,带大家从总整体上了解Spring Boot自动配置的原理以及自动配置核心组件的运作过程。这些内容大家需要重点关注,只有了解这些基础的组件和功能,我们在后续集成其他三方类库的Starters时,才能够更加清晰地了解它们都运用了自动配置的哪些功能。

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

主要内容

1. Spring Boot 应用程序的启动

在 《【Spring Boot 源码学习】@SpringBootApplication 注解》这篇博文中,我们新建了一个基于Spring Boot的测试项目。

image.png

如上图中的<code>DemoApplication就是我们这里Spring Boot项目的入口类。

同时,我们可以看到DemoApplicationmain方法中,直接调用了SpringApplication的静态方法run,用于启动整个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 resourceLoaderResourceLoader为资源加载的接口,它用于在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);

}

}

最后,我们来运行DemoApplicationmain方法。

image.png

从上图可以看出,我们的应用依然能正常启动,并完成自动配置。因此,决定Spring Boot启动的入口类并不是一定是<code>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 应用类型推断

我们继续往下翻看源码,这里调用了WebApplicationTypededuceFromClasspath方法来进行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存在,并且DispatcherServletServletContainer都不存在,则返回类型为WebApplicationType.REACTIVE

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

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

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

image.png

2.3 加载 BootstrapRegistryInitializer

this.bootstrapRegistryInitializers = new ArrayList<>(

getSpringFactoriesInstances(BootstrapRegistryInitializer.class));

上述逻辑用于加载并初始化<code>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 推断应用入口类

最后一步,调用SpringApplicationdeduceMainApplicationClass方法来进行入口类的推断:

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。

点击关注,第一时间了解华为云新鲜技术~



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。