SpringApplication的基础特性

在 Spring Boot 生态中,SpringApplication 是启动应用的核心类,它封装了繁杂的启动流程,让开发者能以简洁方式启动应用,同时还提供了丰富可扩展点,助力定制化需求实现。

在这里我会先讲基础特性,然后会再将一些 SpringApplication 的基础原理

自定义 banner

什么是 banner

banner 是 Spring Boot 应用启动时,在控制台打印的一段字符图案或文字信息,默认是 Spring Boot 标志性图案。它虽不影响应用功能,却能成为应用独特 “名片”,用于展示团队文化、应用名称等个性化内容。

image-20250613165542529

这个就是 banner

这个东西是可以自定义的

如何自定义 banner

在类路径(一般是 src/main/resources 目录)下添加 banner.txt 文件,Spring Boot 启动时会自动加载该文件内容作为 banner 显示。可借助在线工具(如文中提到的 Spring Boot banner 在线生成工具,访问 banner-bootschool.net ),生成个性文字图案(支持英文等),下载后替换 banner.txt 文字,轻松定制专属启动画面。

自定义banner的网站

若不想用默认 banner.txt 文件名或存放路径,可通过配置 spring.banner.location 实现。例如在 application.yml 中配置:

1
2
3
spring:
banner:
location: classpath:custom/banner-custom.txt

这样 Spring Boot 启动时,就会加载 classpath:custom/ 路径下的 banner-custom.txt 文件作为 banner 。

除了文本格式的 banner.txt,Spring Boot 还支持其他格式:

  • banner.gif/banner.jpg/banner.png :若在类路径放置这些图片格式文件,Spring Boot 会将图片转换为字符画形式展示(效果取决于图片复杂度和终端支持度)。
  • 配置控制显示:通过 spring.banner.image.* 系列配置,如 spring.banner.image.widthspring.banner.image.height 等,可调整图片转字符画的显示参数,精细控制 banner 呈现效果 。
1
2
spring.main.banner-mode=off
# 这样就关闭了banner,默认是 console

自定义 SpringApplication:灵活把控启动配置

直接创建与配置 SpringApplication 对象

在 Spring Boot 应用中,常规启动方式是借助 @SpringBootApplication 注解和 SpringApplication.run(MyApplication.class, args) 简洁启动

1
2
3
4
5
6
7
@SpringBootApplication
public class SpringBootWebPart1Application {
public static void main(String[] args) {
// SpringApplication调用run方法,传入你的主程序启动类和不定参数args
SpringApplication.run(SpringBootWebPart1Application.class, args);
}
}

我们来看看SpringApplication中涉及到run的源代码

1
2
3
4
5
6
7
8
9
10
11
public class SpringApplication { 
public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
return run(new Class[]{primarySource}, args);
}

public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
// 先会 new 一个主程序应用,然后把我们的主程序传入进来,再调用run
// 所以这个过程我们可以拆解,就是自定义启动行为
return (new SpringApplication(primarySources)).run(args);
}
}

这两个静态方法本质上是快速创建 SpringApplication 对象并执行启动流程的 “语法糖”。但在实际开发中,若想对应用启动进行更细粒度的配置,就需要手动创建 SpringApplication 对象,深入参与启动过程。

常规的静态 run 方法虽然简洁,但封装了很多默认的启动逻辑。当我们需要自定义一些启动行为时,手动创建 SpringApplication 对象就显得尤为必要。示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import org.springframework.boot.Banner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
// 手动创建 SpringApplication 对象,指定主配置类
// 拆成两步写,这样就可以在run方法前自定义 SpringApplication的底层设置
SpringApplication application = new SpringApplication(MyApplication.class);
// 设置 banner 显示模式为关闭,即启动时不显示 banner
application.setBannerMode(Banner.Mode.OFF);
// 等等各种行为都可以自定义
// 执行启动
application.run(args);
}
}

在这个过程中:

  • 创建对象new SpringApplication(MyApplication.class) 明确指定了应用的主配置类,SpringApplication 会基于此去扫描和加载相关的 Bean 定义、自动配置等内容。
  • 配置行为:通过 setBannerMode 方法,能灵活控制 banner 显示状态。除了 OFF(关闭),还有 CONSOLE(控制台显示,默认)、LOG(记录到日志文件)模式。比如在生产环境,为避免控制台冗余输出,可设置为 OFF;开发环境下,为了直观看到 Spring Boot 标志性启动画面或者自定义的 banner ,则可使用 CONSOLE 模式 。
  • 执行启动:调用 run(args) 方法,才真正触发 Spring Boot 应用的启动流程,包括环境准备、上下文创建、Bean 加载和初始化等一系列操作。

注意,在程序化调整 SpringApplication 的启动参数的时候,它的优先级是低于配置文件的优先级的,这个可以看配置注入的源码,你会发现是为什么是这个顺序。

Builder 方式构建 SpringApplication——FluentBuilder API

在 Spring Boot 中,除了直接通过 SpringApplication 的构造方法、静态 run 方法来创建和启动应用,还提供了 FluentBuilder API(流畅构建器 API) ,也就是基于 SpringApplicationBuilder 的构建方式。它通过链式调用的语法,让开发者更灵活、更直观地配置 SpringApplication 的各项参数

定制化 Banner + 激活配置文件示例

1
2
3
4
5
6
new SpringApplicationBuilder(MyApp.class)
// 自定义 Banner(可以是自己写的 ASCII 字符串,或者读取文件)
.banner(new ResourceBanner(new ClassPathResource("my-banner.txt")))
.bannerMode(Banner.Mode.CONSOLE) // 在控制台显示 Banner
.profiles("dev", "local") // 激活 dev、local 配置文件
.run(args);

SpringApplicationBuilder 支持链式调用,一步一步配置应用的各种属性,最后调用 run(...) 启动应用。

1
2
3
4
5
new SpringApplicationBuilder(MyApp.class)
.bannerMode(Banner.Mode.CONSOLE) // 配置 Banner 显示模式
.logStartupInfo(true) // 开启启动日志
.web(WebApplicationType.SERVLET) // 指定 Web 应用类型
.run(args); // 启动应用

如果不用 Builder,等价逻辑需要这样写:

1
2
3
4
5
SpringApplication app = new SpringApplication(MyApp.class);
app.setBannerMode(Banner.Mode.CONSOLE);
app.setLogStartupInfo(true);
app.setWebApplicationType(WebApplicationType.SERVLET);
app.run(args);

可以看到,Builder 方式通过链式调用,把零散的 set 操作串起来

SpringApplicationBuilder 内部其实是对 SpringApplicationApplicationContext 构建过程的封装,核心思路是:

  1. 封装配置参数: 把 SpringApplication 的各种配置项(如 webApplicationTypebannerModeinitializers 等),以及 ApplicationContext 的相关配置,通过 Builder 的方法暴露出来,让开发者用链式调用设置。

  2. 延迟构建 SpringApplication: 调用 run(...) 之前,Builder 只是在收集配置;真正调用 run(...) 时,才会根据收集的配置,创建 SpringApplication 实例,并执行启动流程。

  3. 支持父子上下文: 这是 Builder 方式的一个重要特性!它可以方便地构建 父子 Spring 上下文(比如父上下文放公共配置,子上下文放 Web 相关配置),典型代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public static void main(String[] args) {
    new SpringApplicationBuilder()
    // 父上下文:加载公共配置
    .parent(CommonConfig.class)
    .web(WebApplicationType.NONE) // 父上下文是非 Web 类型
    // 子上下文:加载 Web 相关配置
    .child(WebConfig.class)
    .web(WebApplicationType.SERVLET) // 子上下文是 Servlet Web 类型
    .run(args);
    }
    • 父上下文 CommonConfig 里的 Bean(如 DataSource),子上下文 WebConfig 可以直接引用。

    • 子上下文的 Bean(如 UserController)不会污染父上下文,实现了一定程度的 “模块隔离”。

SpringApplicationBuilder 提供了丰富的链式方法,常用的有这些:

方法名 作用
sources(...) 指定主配置类(等价于 SpringApplication 构造方法里的 primarySources
web(WebApplicationType) 设置 Web 应用类型(SERVLET/REACTIVE/NONE)
banner(Banner) 自定义启动 Banner(比如替换成自己的 ASCII 艺术字)
bannerMode(Banner.Mode) 控制 Banner 显示方式(CONSOLE/LOG/OFF)
logStartupInfo(boolean) 是否打印启动日志(默认 true
initializers(...) 添加 ApplicationContextInitializer 初始化器
listeners(...) 添加 ApplicationListener 监听器
parent(...) / child(...) 构建父子上下文(父上下文通常放公共 Bean,子上下文放业务 Bean)
profiles(...) 指定激活的配置文件(等价于 spring.profiles.active
environment(...) 自定义 Environment(比如添加额外的属性源)

SpringApplication的基础原理

@SpringBootApplication

可以发现,Spring Boot 项目中的启动类,总会加一个@SpringBootApplication,这个就是Spring Boot启动类的核心部分,它的存在使得项目启动类非常简单,仅仅存在一个注解`@SpringBootApplication以及一个运行参数为被该注解标注类run函数,基本全部 Spring Boot 的启动准备工作就会被自动完成。

对于该启动类的分析,就从这个Spring Boot的核心注解@SpringBootApplication开始入手。

字面分析,这个注解是标注一个Spring Boot应用。

我们进去这个注解的详细信息,去看看它的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
package org.springframework.boot.autoconfigure;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.beans.factory.support.BeanNameGenerator;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.context.TypeExcludeFilter;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.core.annotation.AliasFor;

/**
* 这是一个组合注解,整合了 Spring Boot 应用所需的常用注解配置。
* 一般会把该注解添加到主应用类上,这样就能启用以下功能:
* - 自动配置:依据类路径和 Bean 定义自动配置 Spring 应用上下文
* - 组件扫描:自动扫描并注册带有特定注解的组件
* - 声明配置类:表明该类是一个配置类
*/
@Target({ElementType.TYPE}) // 此注解可用于类、接口或枚举
@Retention(RetentionPolicy.RUNTIME) // 在运行时可以通过反射获取该注解
@Documented // 该注解会被包含在 JavaDoc 中
@Inherited // 该注解可被继承
@SpringBootConfiguration // 表明这是一个 Spring Boot 配置类
@EnableAutoConfiguration // 开启自动配置功能
// 在自动扫描组件时排除特定类型的类
@ComponentScan(
excludeFilters = {@Filter(
type = FilterType.CUSTOM,
classes = {TypeExcludeFilter.class} // 排除测试相关的组件
), @Filter(
type = FilterType.CUSTOM,
classes = {AutoConfigurationExcludeFilter.class} // 专注于处理自动配置类的排除,排除自动配置类
)}
) // 自动扫描并注册组件,同时排除特定类型
public @interface SpringBootApplication {
/**
* 明确指定要排除的自动配置类。
* 这些类不会被 Spring Boot 自动配置。
*/
@AliasFor(
annotation = EnableAutoConfiguration.class
)
Class<?>[] exclude() default {};

/**
* 明确指定要排除的自动配置类的全限定名。
* 这些类不会被 Spring Boot 自动配置。
*/
@AliasFor(
annotation = EnableAutoConfiguration.class
)
String[] excludeName() default {};

/**
* 指定要扫描组件的基础包。
* 如果没有指定,就会从被该注解标注的类所在的包开始扫描。
*/
@AliasFor(
annotation = ComponentScan.class,
attribute = "basePackages"
)
String[] scanBasePackages() default {};

/**
* 指定要扫描组件的基础包的参考类。
* 每个类所在的包都会被作为扫描的基础包。
*/
@AliasFor(
annotation = ComponentScan.class,
attribute = "basePackageClasses"
)
Class<?>[] scanBasePackageClasses() default {};

/**
* 为组件扫描时发现的 Bean 指定命名生成器类。
*/
@AliasFor(
annotation = ComponentScan.class,
attribute = "nameGenerator"
)
Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;

/**
* 指示是否要代理配置类的 Bean 方法,从而实现 Bean 的生命周期管理。
* - true:使用 CGLIB 代理 Bean 方法
* - false:直接调用 Bean 方法(适用于函数式风格的配置)
*/
@AliasFor(
annotation = Configuration.class
)
boolean proxyBeanMethods() default true;
}

进入到这个注解后,可以发现该注解由四个元注解,以及其他三个注解组成分别是

1
2
3
4
5
6
7
8
9
10
11
@SpringBootConfiguration // 表明这是一个 Spring Boot 配置类
@EnableAutoConfiguration // 开启自动配置功能
// 在自动扫描组件时排除特定类型的类
@ComponentScan(
excludeFilters = {@Filter(
type = FilterType.CUSTOM,
classes = {TypeExcludeFilter.class} // 排除测试相关的组件
), @Filter(
type = FilterType.CUSTOM,
classes = {AutoConfigurationExcludeFilter.class} // 专注于处理自动配置类的排除,排除自动配置类
)}

我们首先从@SpringBootConfiguration 注解看起

@SpringBootConfiguration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package org.springframework.boot;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.AliasFor;
import org.springframework.stereotype.Indexed;

/**
* 该注解是 Spring Boot 应用的配置类标识,是 @Configuration 的特殊形式。
* 通常会将此注解添加到主应用类上,用于:
* 1. 声明该类为 Bean 定义的来源
* 2. 启用对 @Bean 方法的增强处理
* 3. 支持组件扫描和自动配置的元数据处理
*
* 建议与 @EnableAutoConfiguration@ComponentScan 组合使用,
* 不过 @SpringBootApplication 已经整合了这三个注解。
*/
@Target({ElementType.TYPE}) // 此注解可用于类、接口或枚举
@Retention(RetentionPolicy.RUNTIME) // 在运行时可以通过反射获取该注解
@Documented // 该注解会被包含在 JavaDoc 中
@Configuration // 表明这是一个配置类,可以定义 Bean
@Indexed // 支持组件索引,提高组件扫描性能,在 META-INF/spring.components 中生成索引文件,避免全类路径扫描
public @interface SpringBootConfiguration {
/**
* 指示是否要代理配置类的 Bean 方法,从而实现 Bean 的生命周期管理。
* - true:使用 CGLIB 代理 Bean 方法,确保对同一 Bean 的多次调用返回相同实例(单例)
* - false:直接调用 Bean 方法,每次调用都可能创建新实例(适用于函数式风格配置)
*
* 此属性与 @Configuration 中的 proxyBeanMethods 属性保持一致。
*/
@AliasFor(
annotation = Configuration.class
)
boolean proxyBeanMethods() default true;
}

可以发现,这个注解中除了元注解之外,存在@Configuration这个Spring Framework 提供的原生注解,@SpringBootConfiguration 对其进行了扩展,增加了对 Spring Boot 特性的支持。

那么可以理解@SpringBootConfiguration为他仅仅就是一个配置类。我们知道,@Configuration注解为Spring中的配置类注解,其中被@Component标注为Spring组件,意味着他被注册到IOC容器。

因此,套用官方文档的答案:@SpringBootConfiguration表示一个类提供 Spring Boot 应用程序@Configuration。可以用作 Spring 标准@Configuration注释的替代方法,以便可以自动找到配置(例如在测试中)。

而且,我们可以明确看出 @SpringBootApplication 是整合了 @SpringBootConfiguration@EnableAutoConfiguration@ComponentScan 的组合注解,而其中SpringBootApplication 中的属性通过 @AliasFor 映射到底层注解,这种设计让开发者可以在不破坏注解组合的前提下,灵活定制配置

1
2
3
4
5
6
7
8
@AliasFor(annotation = EnableAutoConfiguration.class)
Class<?>[] exclude() default {};

@AliasFor(annotation = ComponentScan.class, attribute = "basePackages")
String[] scanBasePackages() default {};

@AliasFor(annotation = Configuration.class) // SpringBootConfiguration 继承自 Configuration
boolean proxyBeanMethods() default true;
  • exclude → 映射到 @EnableAutoConfigurationexclude
  • scanBasePackages → 映射到 @ComponentScanbasePackages
  • proxyBeanMethods → 映射到 @ConfigurationproxyBeanMethods

@EnableAutoConfiguration

接下来就是咱们比较熟悉的注解,@EnableAutoConfiguration,它是自动配置原理的核心注解,它的作用就是开启自动配置,进入源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package org.springframework.boot.autoconfigure;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.context.annotation.Import;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

Class<?>[] exclude() default {};

String[] excludeName() default {};
}

可以发现,除了四个元注解,这个注解被两个注解所标注@AutoConfigurationPackage@Import(AutoConfigurationImportSelector.class)

我们之前的文章中分析过,@AutoConfigurationPackage中使用注解@Import,导入了AutoConfigurationPackages.Registrar.class到你的Spring容器中,来处理包注册,@SpringBootApplication这个组合注解包含了 @AutoConfigurationPackage,所以当你使用 @SpringBootApplication 时,会自动启用包注册功能。在 Spring Boot 应用启动时,这个注解会和 @EnableAutoConfiguration 一起工作,共同构建自动配置的基础环境。

@EnableAutoConfiguration还通过@Import(AutoConfigurationImportSelector.class)引入了关键的导入选择器,其核心逻辑在于AutoConfigurationImportSelectorselectImports()方法。该方法会调用SpringFactoriesLoader.loadFactoryNames(EnableAutoConfiguration.class, classLoader),从类路径的META-INF/spring.factories中获取自动配置类列表。当项目引入某个依赖(如spring-boot-starter-web)时,其 JAR 包内的spring.factories会包含对应的自动配置类,从而被SpringFactoriesLoader扫描到。

所以说,Spring Boot 启动时,通过SpringFactoriesLoader工具类从类路径下的META-INF/spring.factories文件中,读取与EnableAutoConfiguration.class关联的配置类列表,并将这些类导入 Spring 容器,实现自动配置功能。

与`@ComponentScan相比,二者的区别在于,@AutoConfigurationPackage 主要是为自动配置提供包路径信息,而 @ComponentScan 侧重于组件扫描。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package org.springframework.boot.autoconfigure;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.context.annotation.Import;

/**
* 该注解用于标记自动配置包的注册。
* 通常会被 @SpringBootApplication 间接引入,不过也能单独使用。
* 其核心功能是:
* 1. 记录主应用类所在的包路径
* 2. 让自动配置组件能够定位到这些包
* 3. 支持使用 basePackages 或 basePackageClasses 自定义扫描包
*/
@Target({ElementType.TYPE}) // 此注解可用于类、接口或枚举
@Retention(RetentionPolicy.RUNTIME) // 在运行时可以通过反射获取该注解
@Documented // 该注解会被包含在 JavaDoc 中
@Inherited // 该注解可被继承
@Import({AutoConfigurationPackages.Registrar.class}) // 导入 Registrar 类来处理包注册
public @interface AutoConfigurationPackage {
/**
* 手动指定要注册的基础包。
* 如果没有指定,将使用被 @AutoConfigurationPackage 标注的类所在的包。
*/
String[] basePackages() default {};

/**
* 手动指定要注册的基础包的参考类。
* 每个类所在的包都会被作为基础包进行注册。
*/
Class<?>[] basePackageClasses() default {};
}
  • 借助 AutoConfigurationPackages.Registrar 类,把主应用类所在的包路径注册到 Spring 容器中。这些包路径会被存储在 BeanDefinitionRegistry 里。

  • 其他自动配置类可以通过 AutoConfigurationPackages.get(BeanFactory) 方法获取这些包路径。像 JPA 实体扫描、组件扫描等功能都会依赖这些包路径

  • 可以通过 basePackages 参数直接指定包名。这就是为什么 Spring Boot 可以配置自定义扫描路径,也能通过 basePackageClasses 参数指定参考类,以类所在的包作为扫描范围。这样就实现了支持灵活的包路径自定义,以满足不同的项目结构需求。

  • @AutoConfigurationPackage就是添加该注解的类所在的包作为自动配置包进行管理。他的实现就是依赖于工具类AutoConfigurationPackages中的内部类Registrar对所标注的包进行注册。

进入到AutoConfigurationPackages这个类中,就会发现,其中有一些重要的方法来负责管理和存储自动配置的基础包路径,用于扫描组件(如 @Entity@Repository 等)以及定位需要自动配置的类时候使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.springframework.boot.autoconfigure;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Stream;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConstructorArgumentValues;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.boot.context.annotation.DeterminableImports;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;

public abstract class AutoConfigurationPackages {
private static final Log logger = LogFactory.getLog(AutoConfigurationPackages.class);
private static final String BEAN = AutoConfigurationPackages.class.getName();

public static boolean has(BeanFactory beanFactory) {
return beanFactory.containsBean(BEAN) && !get(beanFactory).isEmpty();
}

public static List<String> get(BeanFactory beanFactory) {
try {
return ((BasePackages)beanFactory.getBean(BEAN, BasePackages.class)).get();
} catch (NoSuchBeanDefinitionException var2) {
throw new IllegalStateException("Unable to retrieve @EnableAutoConfiguration base packages");
}
}

public static void register(BeanDefinitionRegistry registry, String... packageNames) {
if (registry.containsBeanDefinition(BEAN)) {
addBasePackages(registry.getBeanDefinition(BEAN), packageNames);
} else {
RootBeanDefinition beanDefinition = new RootBeanDefinition(BasePackages.class);
beanDefinition.setRole(2);
addBasePackages(beanDefinition, packageNames);
registry.registerBeanDefinition(BEAN, beanDefinition);
}

}

private static void addBasePackages(BeanDefinition beanDefinition, String[] additionalBasePackages) {
ConstructorArgumentValues constructorArgumentValues = beanDefinition.getConstructorArgumentValues();
if (constructorArgumentValues.hasIndexedArgumentValue(0)) {
String[] existingPackages = (String[])constructorArgumentValues.getIndexedArgumentValue(0, String[].class).getValue();
constructorArgumentValues.addIndexedArgumentValue(0, Stream.concat(Stream.of(existingPackages), Stream.of(additionalBasePackages)).distinct().toArray((x$0) -> new String[x$0]));
} else {
constructorArgumentValues.addIndexedArgumentValue(0, additionalBasePackages);
}

}

static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
AutoConfigurationPackages.register(registry, (String[])(new PackageImports(metadata)).getPackageNames().toArray(new String[0]));
}

public Set<Object> determineImports(AnnotationMetadata metadata) {
return Collections.singleton(new PackageImports(metadata));
}
}

private static final class PackageImports {
private final List<String> packageNames;

PackageImports(AnnotationMetadata metadata) {
AnnotationAttributes attributes = AnnotationAttributes.fromMap(metadata.getAnnotationAttributes(AutoConfigurationPackage.class.getName(), false));
List<String> packageNames = new ArrayList(Arrays.asList(attributes.getStringArray("basePackages")));

for(Class<?> basePackageClass : attributes.getClassArray("basePackageClasses")) {
packageNames.add(basePackageClass.getPackage().getName());
}

if (packageNames.isEmpty()) {
packageNames.add(ClassUtils.getPackageName(metadata.getClassName()));
}

this.packageNames = Collections.unmodifiableList(packageNames);
}

List<String> getPackageNames() {
return this.packageNames;
}

public boolean equals(Object obj) {
return obj != null && this.getClass() == obj.getClass() ? this.packageNames.equals(((PackageImports)obj).packageNames) : false;
}

public int hashCode() {
return this.packageNames.hashCode();
}

public String toString() {
return "Package Imports " + String.valueOf(this.packageNames);
}
}

static final class BasePackages {
private final List<String> packages;
private boolean loggedBasePackageInfo;

BasePackages(String... names) {
List<String> packages = new ArrayList();

for(String name : names) {
if (StringUtils.hasText(name)) {
packages.add(name);
}
}

this.packages = packages;
}

List<String> get() {
if (!this.loggedBasePackageInfo) {
if (this.packages.isEmpty()) {
if (AutoConfigurationPackages.logger.isWarnEnabled()) {
AutoConfigurationPackages.logger.warn("@EnableAutoConfiguration was declared on a class in the default package. Automatic @Repository and @Entity scanning is not enabled.");
}
} else if (AutoConfigurationPackages.logger.isDebugEnabled()) {
String packageNames = StringUtils.collectionToCommaDelimitedString(this.packages);
AutoConfigurationPackages.logger.debug("@EnableAutoConfiguration was declared on a class in the package '" + packageNames + "'. Automatic @Repository and @Entity scanning is enabled.");
}

this.loggedBasePackageInfo = true;
}

return this.packages;
}
}
}

其中的静态工具方法

1
2
3
public static boolean has(BeanFactory beanFactory)
public static List<String> get(BeanFactory beanFactory)
public static void register(BeanDefinitionRegistry registry, String... packageNames)
  • has():检查 BeanFactory 中是否存在自动配置包的注册信息。
  • get():获取已注册的基础包路径列表。
  • register():向 BeanDefinitionRegistry 注册基础包路径。

进入到内部类结构,可以得到如下

  • Registrar:实现 ImportBeanDefinitionRegistrar,负责注册 BasePackages Bean。
  • PackageImports:解析 @AutoConfigurationPackage 注解的属性,提取基础包路径。
  • BasePackages:封装基础包路径列表,支持日志记录和路径获取。

简单讲解一下其工作流程

  • 首先当 @AutoConfigurationPackage 注解被处理时,Registrar 方法会被调用,来注册包路径,从注解元数据中提取包路径(通过 PackageImports)。调用 AutoConfigurationPackages.register() 注册路径。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
    public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
    AutoConfigurationPackages.register(registry, (String[])(new PackageImports(metadata)).getPackageNames().toArray(new String[0]));
    }

    public Set<Object> determineImports(AnnotationMetadata metadata) {
    return Collections.singleton(new PackageImports(metadata));
    }
    }
  • 然后 PackageImports 解析注解属性,从中可以发现优先级,basePackages > basePackageClasses > 注解所在类的包,按照这个顺序获得packageNames属性也就是启动类所在的包。,如果用户没有显式指定包,Spring Boot 会使用主应用类所在的包,这就是默认行为的来源

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    private static final class PackageImports {
    PackageImports(AnnotationMetadata metadata) {
    // 1. 获取 @AutoConfigurationPackage 注解的 basePackages 属性
    List<String> packageNames = new ArrayList(Arrays.asList(attributes.getStringArray("basePackages")));

    // 2. 获取 basePackageClasses 属性并提取其所在包
    for(Class<?> basePackageClass : attributes.getClassArray("basePackageClasses")) {
    packageNames.add(basePackageClass.getPackage().getName());
    }

    // 3. 如果都没指定,则使用注解所在类的包
    if (packageNames.isEmpty()) {
    packageNames.add(ClassUtils.getPackageName(metadata.getClassName()));
    }
    }
    }

    BasePackages 管理包路径,这个没啥好讲,就是存储一下基础包路径列表,然后给点日志,这个方法中有这样一条List<String> packages = new ArrayList();,这个列表将所有自动配置的基础包路径集中存储在一个 Bean 中,便于其他自动配置类统一获取。

  • 最后注册BeanDefinition,保证基础设施 Bean不对外暴露,通过构造函数参数形式存储包路径数组

Spring Boot 应用程序的启动

继续剖析 run 方法

讲完了注解相关的原理和源代码梳理后,让我们聚焦于应用程序的启动过程,我们来看看Spring Boot项目的启动原理

我们可以看到 SpringBootWebPart1Applicationmain 方法中,直接调用了 SpringApplication 的静态方法 run,用于启动整个 Spring Boot 项目。Spring Boot 应用的启动通常始于以下代码:

1
2
3
4
5
6
7
@SpringBootApplication
public class SpringBootWebPart1Application {

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

run方法的源代码我们上面已经讲过了,实际上就是 new 了一个SpringApplication 对象,其构造参数 primarySources 为加载的主要资源类,通常就是 SpringBoot 的入口类,并调用其 run 方法,通过其参数 args 为传递给应用程序的参数信息,然后进行启动后返回一个应用上下文对象 ConfigurableApplicationContext

简单来说,这个静态方法实际上执行了两个关键步骤:

  1. 创建SpringApplication实例:封装应用上下文的创建逻辑。
  2. 调用run()方法:启动应用并返回ConfigurableApplicationContext

创建SpringApplication实例的细节原理

在启动的过程中,SpringApplication会进行实例化,SpringApplication的初始化过程涉及到其构造函数,它承担了应用启动前的准备工作,如下源码所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public SpringApplication(Class<?>... primarySources) {
this((ResourceLoader)null, primarySources);
}

public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
this.addCommandLineProperties = true;
this.addConversionService = true;
this.headless = true;
this.additionalProfiles = Collections.emptySet();
this.applicationContextFactory = ApplicationContextFactory.DEFAULT;
this.applicationStartup = ApplicationStartup.DEFAULT;
this.properties = new ApplicationProperties();
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "'primarySources' must not be null");
this.primarySources = new LinkedHashSet(Arrays.asList(primarySources));
this.properties.setWebApplicationType(WebApplicationType.deduceFromClasspath());
this.bootstrapRegistryInitializers = new ArrayList(this.getSpringFactoriesInstances(BootstrapRegistryInitializer.class));
this.setInitializers(this.getSpringFactoriesInstances(ApplicationContextInitializer.class));
this.setListeners(this.getSpringFactoriesInstances(ApplicationListener.class));
this.mainApplicationClass = this.deduceMainApplicationClass();
}

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

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

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

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

而其中 primarySources 这个可变参数的描述有点疑惑,其实primarySources参数接收的是被@EnableAutoConfiguration(或@SpringBootApplication)标注的类,其核心作用是:

  • 作为自动配置的触发点:告知 Spring Boot 从该类的注解中获取组件扫描范围、自动配置规则等信息。
  • 确定基础包路径:该类所在的包会被@AutoConfigurationPackage注册为自动配置的基础扫描路径(参考前文AutoConfigurationPackages的解析)。

我们来做一个实验,在启动类目录的同级下,再建一个如下SpringBootWebSecondApplication

1
2
3
4
5
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringBootWebSecondApplication {
}

标记上@SpringBootApplication注解,然后,改写SpringBootWebPart1Application原来的启动类

1
2
3
4
5
public class SpringBootWebPart1Application {
public static void main(String[] args) {
SpringApplication.run(SpringBootWebSecondApplication.class, args);
}
}

项目跑起来是没有任何问题的,并完成自动配置,因此,决定 Spring Boot 启动的入口类并不是一定是 main 方法所在类,而是直接或间接被 @EnableAutoConfiguration 标注的类。即使main方法所在类未被@SpringBootApplication标注,只要SpringApplication.run()传入的primarySources是被@EnableAutoConfiguration标注的类,应用仍可正常启动。

image-20250620171538976

这是因为,Spring Boot 的启动逻辑仅依赖primarySources中的类是否携带@EnableAutoConfiguration,而main方法仅作为 Java 程序的入口,二者无直接关联。

primarySources类所在的包会成为组件扫描的默认基础包,等价于@ComponentScan未指定basePackages时的行为。Spring Boot 启动时候会自动扫描com.example.myapp及其子包下的@Component@Service等组件。然后primarySources类上的@EnableAutoConfiguration会触发AutoConfigurationImportSelector加载spring.factories中的自动配置类。若传入的类未标注该注解,自动配置机制将失效。

其实发现源码中,还有一个方法addPrimarySources,发现可以调用 addPrimarySources 方法来追加额外的 primarySources,所以,primarySources支持传入多个类(通过可变参数),多个类的包路径会被合并为组件扫描范围,多个类上的@EnableAutoConfiguration配置会被合并(如exclude参数)

1
2
3
public void addPrimarySources(Collection<Class<?>> additionalPrimarySources) {
this.primarySources.addAll(additionalPrimarySources);
}

构造参数中,这里将 primarySources 参数转换为 LinkedHashSet 集合,并赋值给SpringApplication 的私有成员变量 Set<Class<?>> primarySources

1
this.primarySources = new LinkedHashSet(Arrays.asList(primarySources));

Spring Boot项目启动的初始化部分

这次再来看初始化的部分,只挑重要的初始化部分步骤就是如下部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
// 设置默认属性
this.addCommandLineProperties = true;
this.headless = true;
this.applicationContextFactory = ApplicationContextFactory.DEFAULT;

// 保存主源类(通常是@SpringBootApplication标注的类)
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));

// 推断应用类型(SERVLET、REACTIVE或NONE)
this.webApplicationType = WebApplicationType.deduceFromClasspath();

// 从spring.factories加载初始化器和监听器
this.setInitializers(this.getSpringFactoriesInstances(ApplicationContextInitializer.class));
this.setListeners(this.getSpringFactoriesInstances(ApplicationListener.class));

// 推断main方法所在的类
this.mainApplicationClass = deduceMainApplicationClass();
}

确定 Web 应用类型

1
this.webApplicationType = WebApplicationType.deduceFromClasspath();

这里调用了 WebApplicationTypededuceFromClasspath 方法来进行 Web 应用类型的推断

通过检查类路径中的关键类(如javax.servlet.Servletorg.springframework.web.reactive.DispatcherHandler),自动判断应用类型:

  • SERVLET:传统 Servlet Web 应用(默认)
  • REACTIVE:响应式 Web 应用
  • NONE:非 Web 应用

继续翻看其 WebApplicationType 的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.springframework.boot;

import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.aot.hint.TypeReference;
import org.springframework.util.ClassUtils;

public enum WebApplicationType {
NONE,
SERVLET,
REACTIVE;

private static final String[] SERVLET_INDICATOR_CLASSES = new String[]{"jakarta.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("org.springframework.web.reactive.DispatcherHandler", (ClassLoader)null) && !ClassUtils.isPresent("org.springframework.web.servlet.DispatcherServlet", (ClassLoader)null) && !ClassUtils.isPresent("org.glassfish.jersey.servlet.ServletContainer", (ClassLoader)null)) {
return REACTIVE;
} else {
for(String className : SERVLET_INDICATOR_CLASSES) {
if (!ClassUtils.isPresent(className, (ClassLoader)null)) {
return NONE;
}
}

return SERVLET;
}
}

static class WebApplicationTypeRuntimeHints implements RuntimeHintsRegistrar {
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
for(String servletIndicatorClass : WebApplicationType.SERVLET_INDICATOR_CLASSES) {
this.registerTypeIfPresent(servletIndicatorClass, classLoader, hints);
}

this.registerTypeIfPresent("org.glassfish.jersey.servlet.ServletContainer", classLoader, hints);
this.registerTypeIfPresent("org.springframework.web.reactive.DispatcherHandler", classLoader, hints);
this.registerTypeIfPresent("org.springframework.web.servlet.DispatcherServlet", classLoader, hints);
}

private void registerTypeIfPresent(String typeName, ClassLoader classLoader, RuntimeHints hints) {
if (ClassUtils.isPresent(typeName, classLoader)) {
hints.reflection().registerType(TypeReference.of(typeName), new MemberCategory[0]);
}

}
}
}

我们发现,WebApplicationType 是一个定义了可能的Web应用类型的枚举类,这一判断将直接影响

  • 应用上下文类型:决定创建 ServletWebServerApplicationContext(Servlet)、ReactiveWebServerApplicationContext(响应式)或普通 ApplicationContext(非 Web)。
  • 自动配置规则:不同类型的应用会加载不同的自动配置类(如 WebMvcAutoConfiguration 或 WebFluxAutoConfiguration)。
  • 服务启动方式:确定是否启动嵌入式 Web 服务器(如 Tomcat、Netty)。

上来就是一堆常量,这些常量其实就是判断Web应用类型的指示器功能,起到存在则是的作用

1
2
3
4
5
6
private static final String[] SERVLET_INDICATOR_CLASSES = {
"jakarta.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";
  • SERVLET_INDICATOR_CLASSES
    • Servlet:所有 Servlet 容器(如 Tomcat)的基础接口,存在即表示可能为 Servlet 应用。
    • ConfigurableWebApplicationContext:Spring Web 上下文的标准接口,标志着 Web 相关配置的存在。
  • WEBMVC_INDICATOR_CLASS
    • DispatcherServlet 是 Spring MVC 的核心控制器,存在即表示传统 Web MVC 应用。
  • WEBFLUX_INDICATOR_CLASS
    • DispatcherHandler 是 Spring WebFlux 的核心处理器,存在即表示响应式 Web 应用。

deduceFromClasspath ()实现了推断web应用类型的逻辑,分布推断的流程如下:

  1. 响应式应用优先原则
    • 检查是否存在 org.springframework.web.reactive.DispatcherHandler(响应式核心处理器)。
    • 同时确保不存在 DispatcherServlet(Servlet Web MVC 核心)和 ServletContainer(Jersey 容器)。
    • 场景:当项目依赖 spring-boot-starter-webflux 时,此条件成立。
  2. Servlet 应用的必要条件
    • 验证SERVLET_INDICATOR_CLASSES中的所有类是否存在:
      • jakarta.servlet.Servlet:Servlet 规范核心接口。
      • org.springframework.web.context.ConfigurableWebApplicationContext:Web 上下文接口。
    • 场景:引入 spring-boot-starter-web 后,这两个类必然存在
  3. 非 Web 应用的判定
    • 若上述条件均不满足(如仅包含业务依赖,无 Web 相关库),则判定为 NONE

deduceFromClasspath()方法中调用了ClassUtils.isPresent ()方法实现了类存在性检查,该工具类方法就是通过反射创建指定的类,根据在创建过程中是否抛出异常来判断该类是否存在。

1
2
3
4
5
6
7
8
9
10
public static boolean isPresent(String className, @Nullable ClassLoader classLoader) {
try {
forName(className, classLoader);
return true;
} catch (IllegalAccessError err) {
throw new IllegalStateException("Readability mismatch in inheritance hierarchy of class [" + className + "]: " + err.getMessage(), err);
} catch (Throwable var4) {
return false;
}
}
  • 通过 Class.forName() 尝试加载类,若抛出 ClassNotFoundExceptionNoClassDefFoundError,则认为类不存在。
  • false 参数表示不初始化类,仅验证类是否存在,避免触发静态代码块或构造函数。

判断完Web 应用类型之后,就决定了 createApplicationContext() 方法创建的上下文类型,实现了类型推断

加载应用初始化器(ApplicationContextInitializer

1
this.setInitializers(this.getSpringFactoriesInstances(ApplicationContextInitializer.class));

这个就涉及到上面自动配置哪里讲的了,从META-INF/spring.factories加载所有实现ApplicationContextInitializer的类,用于在上下文刷新前进行自定义配置。

加载流程如下

  • 首先,从 spring.factories 读取,getSpringFactoriesInstances() 方法扫描类路径下所有 META-INF/spring.factories,提取 ApplicationContextInitializer 的实现类。加载内置的初始化器,Spring的内置初始化器有很多,分别负责不同的作用

  • 然后通过反射创建实例,按 @Order 注解或 Ordered 接口排序。

  • 之后在 prepareContext() 阶段调用:

    1
    2
    3
    4
    5
    6
    7
    8
    private void prepareContext(ConfigurableApplicationContext context, ...) {
    // ...(其他准备上下文的逻辑,比如加载环境、配置日志等)

    // 关键调用:应用所有的 ApplicationContextInitializer 初始化器
    applyInitializers(context);

    // ...(后续继续准备上下文的逻辑,比如加载 bean 定义等)
    }

    在这一步中,prepareContextSpringApplication 中负责「准备应用上下文」的核心方法之一,在 Spring Boot 启动流程里,会在创建好 ConfigurableApplicationContext(如 AnnotationConfigServletWebServerApplicationContext 等)后,执行一系列准备动作,applyInitializers(context) 就是其中一步,目的是让所有注册好的 ApplicationContextInitializer 有机会在上下文正式刷新(refresh)前,对其做自定义初始化配置。

    可以简单理解为:Spring Boot 先把上下文的 “壳” 建好,然后在真正往里面加载 Bean、刷新容器之前,让各种初始化器先 “装饰” 一下这个上下文,给后续流程提前设置好一些参数、环境等。

    之后

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    protected void applyInitializers(ConfigurableApplicationContext context) {
    // 遍历所有获取到的 ApplicationContextInitializer 初始化器
    for (ApplicationContextInitializer initializer : getInitializers()) {
    // 解析 initializer 所要求的上下文类型(泛型解析)
    // 比如有的 Initializer 可能限定只能处理 ServletWebServerApplicationContext,这里就会解析出对应的类型
    Class<?> requiredType = GenericTypeResolver.resolveTypeArgument(
    initializer.getClass(), ApplicationContextInitializer.class);

    // 断言当前上下文 context 是符合 initializer 要求的类型
    // 如果类型不匹配,会抛出异常,阻止启动,避免初始化器执行时报错
    Assert.isInstanceOf(requiredType, context, "Unable to call initializer");

    // 真正执行初始化逻辑,把当前上下文交给 initializer 去“初始化”
    initializer.initialize(context);
    }
    }
    • SpringApplication 中获取所有已经注册好的 ApplicationContextInitializer 列表。这些初始化器可能来自:

      • spring.factories 中配置的默认实现(Spring Boot 自身或第三方框架注册的);

      • 开发者通过 SpringApplication.addInitializers(...) 手动添加的自定义实现。

    • 利用 Spring 的 GenericTypeResolver 工具,解析 ApplicationContextInitializer 实现类上的泛型参数。

    • 用 Spring 的 Assert 断言工具,检查当前要初始化的 context(实际的应用上下文对象,如 ServletWebServerApplicationContext)是否是 requiredType 及其子类 / 实现类。 如果类型不匹配,直接抛出异常,让开发者能快速发现 “某个初始化器期望的上下文类型和实际创建的上下文不兼容” 问题

    • 回调初始化器的 initialize 方法,把当前准备好的应用上下文对象传递进去,让初始化器执行自定义逻辑。 比如有的初始化器可能会往上下文里添加自定义的 PropertySource(属性源)、提前注册一些特殊的 Bean 定义,或者设置上下文的一些环境变量等,为后续 Spring 容器正式刷新(refresh)做准备。

ApplicationContextInitializer 是 Spring 框架中的一个接口,是 Spring 框架提供的一个强大扩展点,它的主要作用是在Spring容器刷新之前初始化 ConfigurableApplicationContext,允许开发者在 Spring 容器刷新(refresh())之前 对应用上下文进行自定义配置。这个刷新时机往细致了说就是,在 SpringApplication.run(...) 流程中,创建完 ConfigurableApplicationContext 后,执行 refresh 之前。也就是说,此时 Bean 还没开始加载、创建,但是上下文的 “基础环境”(如 EnvironmentBeanFactory 等)已经准备好,初始化器可以在这个阶段对上下文做一些 “前期定制”。

1
2
3
4
@FunctionalInterface
public interface ApplicationContextInitializer<C extends ConfigurableApplicationContext> {
void initialize(C applicationContext);
}
  • SpringApplication.run() 方法中,prepareContext() 阶段调用初始化器(早于 Bean 定义加载和自动配置)。
  • 接收 ConfigurableApplicationContext,可修改上下文配置(如添加属性源、注册 Bean 等)。

这样整套流程下来,你就能明白:ApplicationContextInitializer 是 Spring Boot 留给开发者(以及框架自身)在容器启动最早期介入、定制上下文的 “钩子”,而 prepareContext + applyInitializers 就是触发这些钩子的关键逻辑 ,通过泛型解析、类型校验、遍历执行,确保各种初始化器能安全、有序地对应用上下文做初始化配置。

注册应用监听器(ApplicationListener

1
this.setListeners(this.getSpringFactoriesInstances(ApplicationListener.class));

加载所有实现ApplicationListener的类,监听应用生命周期事件(如启动、失败、停止等)。

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

我不熟监听器,不说了

确定主应用类

1
this.mainApplicationClass = deduceMainApplicationClass();

通过分析调用栈,找出包含main方法的类,用于后续日志和上下文信息。

这是Spring Boot启动的最后一步,调用 SpringApplicationdeduceMainApplicationClass 方法来进行入口类的推断:

1
2
3
private Class<?> deduceMainApplicationClass() {
return (Class)((Optional)StackWalker.getInstance(Option.RETAIN_CLASS_REFERENCE).walk(this::findMainClass)).orElse((Object)null);
}

Spring Boot 启动后,会在日志里输出类似这样的内容:

1
Started DemoApplication in 2.345 seconds (JVM running for 3.456)

这里的 DemoApplication 就是 主应用类(即 main 方法所在类)。它的作用包括:

  • 日志标识:启动日志里明确标注 “哪个类触发了启动”,方便排查问题。
  • 上下文关联:某些自动配置(比如 SpringApplicationAdminJmxAutoConfiguration)会基于主类生成 MBean 名称,用于 JMX 监控。
  • 框架扩展:第三方库(如 APM 工具、自定义启动器)可能需要获取主类,做链路追踪、启动增强等。

那么问题来了,deduceMainApplicationClass 方法是如何找到其主类的呢?

核心逻辑分两步

  1. StackWalker 遍历调用栈

    • StackWalker 是 Java 9+ 引入的 API,能高效遍历当前线程的调用栈,且支持保留类引用(RETAIN_CLASS_REFERENCE 选项)。StackWalker 是专门为 高效遍历栈帧 设计的 API,通过 StackFrame.getDeclaringClass() 直接获取 Class,无需反射。这让 Spring Boot 的启动流程更高效、更简洁。
    • walk(this::findMainClass):把调用栈交给 findMainClass 方法处理,找到符合条件的类。
  2. findMainClass 筛选主类

    • findMainClass 是一个方法引用,实际逻辑是遍历调用栈,找 包含 main 方法的类

      1
      2
      3
      4
      5
      6
      7
      8
      9
      private Optional<Class<?>> findMainClass(Stream<StackWalker.StackFrame> stackFrames) {
      return stackFrames
      // 过滤出包含 "main" 方法的栈帧
      .filter(frame -> frame.getMethodName().equals("main"))
      // 提取对应的类
      .map(StackWalker.StackFrame::getDeclaringClass)
      // 只保留第一个匹配的(因为 main 方法唯一)
      .findFirst();
      }

      调用栈里,main 方法所在类的栈帧会包含 methodName = "main",通过过滤、提取、去重,就能定位到主应用类。

所以说,虽然 Spring Boot 能自动推断主类,但某些场景(比如框架封装、多主类启动)可能需要手动指定。可以通过以下方式实现:

1
2
3
SpringApplication app = new SpringApplication(MyMainClass.class);
app.setMainApplicationClass(MyMainClass.class); // 手动设置主类
app.run(args);

这种方式会跳过 deduceMainApplicationClass 的自动推断,强制使用指定的类作为主应用类

所以说,deduceMainApplicationClass 是 Spring Boot 启动流程的 “收尾环节”,通过 StackWalker 遍历调用栈,精准定位到 main 方法所在类。它的设计既保证了 高效性(避免全栈遍历),又能 直接关联类引用(无需反射),让主类识别逻辑简洁又可靠

Spring Boot 应用程序的启动与生命周期加载机制

这一部分,如果你没有看 Spring Boot part18-生命周期监听与9大事件 ,那就别往下看了

在上面,我们提到了如下内容

@SpringBootApplication

  • 就是:@Configuration ,容器中的组件,配置类。spring ioc 启动就会加载创建这个类对象

@EnableAutoConfiguration: 开启自动配置

@AutoConfigurationPackage : 扫描主程序包:加载自己的组件

  • 利用 @Import(AutoConfigurationPackages.Registrar.class) 想要给容器中导入组件。
  • 把主程序所在的包的所有组件导入进来。
  • 为什么 SpringBoot 默认只扫描主程序所在的包及其子包

@Import(AutoConfigurationImportSelector.class):加载所有自动配置类:加载 starter 导入的组件

1
2
List<String> configurations = ImportCandidates.load(AutoConfiguration.class, getBeanClassLoader())
.getCandidates();

扫描 SPI 文件:

1
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

@ComponentScan

组件扫描:排除一些组件(哪些不要)

排除前面已经扫描进来的 配置类、和 自动配置类 。

1
2
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })

当调用 SpringApplication.run(MyApp.class, args) 时,核心流程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public ConfigurableApplicationContext run(String... args) {
Startup startup = SpringApplication.Startup.create();
if (this.properties.isRegisterShutdownHook()) {
shutdownHook.enableShutdownHookAddition();
}

// 2. 创建并配置引导上下文(BootstrapContext)
DefaultBootstrapContext bootstrapContext = this.createBootstrapContext();
ConfigurableApplicationContext context = null;
this.configureHeadlessProperty();
// 5. 获取并启动所有SpringApplicationRunListener
// 这一步会触发starting()事件
SpringApplicationRunListeners listeners = this.getRunListeners(args);
listeners.starting(bootstrapContext, this.mainApplicationClass);

try {
// 6. 封装命令行参数
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
// 7. 准备环境(包括配置文件加载、属性源处理)
// 触发environmentPrepared()事件
ConfigurableEnvironment environment = this.prepareEnvironment(listeners, bootstrapContext, applicationArguments);
Banner printedBanner = this.printBanner(environment);
// 10. 创建应用上下文(根据应用类型选择不同的容器实现)
context = this.createApplicationContext();
context.setApplicationStartup(this.applicationStartup);
// 12. 准备上下文(加载主配置类、应用初始化器)
// 触发contextPrepared()和contextLoaded()事件
prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
// 13. 刷新上下文(核心IoC容器初始化:Bean定义加载、实例化、依赖注入)
refreshContext(context);
this.afterRefresh(context, applicationArguments);
startup.started();
if (this.properties.isLogStartupInfo()) {
(new StartupInfoLogger(this.mainApplicationClass, environment)).logStarted(this.getApplicationLog(), startup);
}

listeners.started(context, startup.timeTakenToStarted());
this.callRunners(context, applicationArguments);
} catch (Throwable ex) {
throw this.handleRunFailure(context, ex, listeners);
}

我们在这之后,就在 part18 详细展开讲了 Spring Boot 生命周期过程,其中 ioc 容器刷新部分也分为12小步,前些步都是工厂的内容,后面步骤都是其中处理组件的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
@Override
public void refresh() throws BeansException, IllegalStateException {
// 1. 加锁,确保刷新过程互斥执行
this.startupShutdownLock.lock();
try {
// 记录当前刷新线程
this.startupShutdownThread = Thread.currentThread();

// 2. 启动上下文刷新的性能监控
StartupStep contextRefresh = this.applicationStartup.start("spring.context.refresh");

// 3. 环境准备:设置启动时间、清除缓存、验证必要属性
prepareRefresh();

// 4. 获取工厂
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

// 5. 配置 BeanFactory 的基本属性(类加载器、表达式解析器等),准备工厂
prepareBeanFactory(beanFactory);

try {
// 6. 子类扩展点:允许子类对 BeanFactory 进行额外配置
postProcessBeanFactory(beanFactory);

// 7. 执行 BeanFactoryPostProcessor(处理 Bean 定义)
invokeBeanFactoryPostProcessors(beanFactory);

// 8. 注册 BeanPostProcessor(处理 Bean 实例)
registerBeanPostProcessors(beanFactory);

// 9. 初始化国际化消息源
initMessageSource();

// 10. 初始化事件广播器(用于发布事件)
initApplicationEventMulticaster();

// 11. 子类扩展点:初始化特殊 Bean(如 Web 容器)
onRefresh();

// 12. 注册事件监听器
registerListeners();

// 13. 实例化所有非懒加载的单例 Bean(核心步骤),也就是在这一步,造组件的
finishBeanFactoryInitialization(beanFactory);

// 14. 完成刷新:清除资源缓存、发布 ContextRefreshedEvent 事件
finishRefresh();
}

进去到invokeBeanFactoryPostProcessors(beanFactory);中,在这里会提前的自动配置的功能导入进来,这里也是加载spi的时机,相当于导入了自动配置,所有的自动配置类都是在这一步自动扫描进去的,这里只是扫描,创建还在下面finishBeanFactoryInitialization(beanFactory)

image-20250621174108662

finishBeanFactoryInitialization(beanFactory)中,完成了组件的最终创建和初始化,相关代码如下

1
2
3
4
String[] weaverAwareNames = beanFactory.getBeanNamesForType(LoadTimeWeaverAware.class, false, false);
for (String weaverAwareName : weaverAwareNames) {
beanFactory.getBean(weaverAwareName, LoadTimeWeaverAware.class);
}
  • 提前初始化实现了LoadTimeWeaverAware接口的 Bean,这些 Bean 需要在类加载阶段进行 AOP 织入(例如 AspectJ 的 LTW)。通过getBean()强制实例化这些 Bean,确保它们的LoadTimeWeaver在类加载前准备就绪。
1
beanFactory.preInstantiateSingletons();
  • 遍历所有非懒加载的单例 Bean 定义,并按以下顺序创建它们:
    1. 优先创建实现了PriorityOrdered接口的 Bean。
    2. 再创建实现了Ordered接口的 Bean。
    3. 最后创建剩余的普通 Bean。

这样,我们的Spring Boot 生命周期过程就和自动配置,启动过程全部连接起来了

image-20250621171858227