封面画师:T5-茨舞(微博)     封面ID:104963785

参考视频:黑马程序员Spring视频教程,全面深度讲解spring5底层原理

源码仓库:mofan212/advanced-spring (github.com)

37. Boot 骨架项目

使用 IDEA 创建 SpringBoot 项目时,会创建出 .mvn 目录、HELP.mdmvnwmvnw.cmd 等不必要的文件。

如果是 Linux 环境下,执行以下命令获取 SpringBoot 的骨架,并添加 webmysqlmybatis 依赖:

1
curl -G https://start.spring.io/pom.xml -d dependencies=web,mysql,mybatis -o pom.xml

也可以使用 Postman 等接口测试工具来实现。

更多用法执行以下命令进行参考:

1
curl https://start.spring.io

但说实话,实际开发时宁愿直接使用 IDEA 创建项目。

38. Boot War 项目

38.1 项目的构建

利用 IDEA 创建新模块 test_war,区别在于选择的打包方式是 War

创建test_war模块

选择依赖时,勾选 Spring Web。

一般来说,选择 War 作为打包方式都是为了使用 JSP,因为 JSP 不能配合 Jar 打包方式使用。

JSP 文件的存放路径是固定的,在 src/main 目录下的 webapp 目录,如果没有 webapp 目录,需要自行创建。之后新建 hello.jsp

1
2
3
4
5
6
7
8
9
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<h3>Hello!</h3>
</body>
</html>

之后新建控制器类 HelloController,编写控制器方法 hello(),返回值类型是 String,要求返回的是视图名称:

1
2
3
4
5
6
7
@Controller
public class HelloController {
@RequestMapping("/hello")
public String hello() {
return "hello";
}
}

最后要在配置文件中配置视图的前缀、后缀,使控制器方法返回的视图名称对应视图名称的 JSP 页面:

1
2
spring.mvc.view.prefix=/
spring.mvc.view.suffix=.jsp

38.2 项目的测试

使用外置 Tomcat 测试

首先得安装外置 Tomcat,省略安装步骤。

然后在 IDEA 的 Run/Debug Configurations 中进行配置,选择安装的外置 Tomcat:

配置Tomcat-Server

然后在 Deployment 中指定当前项目的部署方式和应用程序上下文路径:

修改Tomcat-Server的Deployment

尽管使用外置 Tomcat 进行测试,但主启动类不能少:

1
2
3
4
5
6
@SpringBootApplication
public class TestWarApplication {
public static void main(String[] args) {
SpringApplication.run(TestWarApplication.class, args);
}
}

除此之外,还要编写 ServletInitializer,在外置 Tomcat 启动时,找到 SpringBoot 项目的主启动类,执行 SpringBoot 流程:

1
2
3
4
5
6
public class ServletInitializer extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(TestWarApplication.class);
}
}

如果没有 ServletInitializer 类,则无法使 SpringBoot 项目使用外置 Tomcat。

运行程序后,访问 localhost:8080/hello,页面进入编写的 hello.jsp 页面。

使用内嵌 Tomcat 测试

打包方式为 Jar 时,直接运行主启动类,然后访问对应的请求路径即可跳转到指定的视图中,那打包访问变成 War 之后,使用这种方式还能够成功跳转吗?

程序运行成功后,访问 localhost:8080/hello,页面并没有按照预期跳转到 hello.jsp 页面中,也是下载了该页面。

这是因为内嵌 Tomcat 中不具备 JSP 解析能力,如果要想使其具备解析 JSP 的能力,需要添加依赖:

1
2
3
4
5
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<scope>provided</scope>
</dependency>

之后再访问 localhost:8080/hello,页面进入编写的 hello.jsp 页面。

使用内嵌 Tomcat 测试遇到的问题

  • 添加 tomcat-embed-jasper 依赖后,访问 localhost:8080/hello,仍在下载 hello.jsp

答:清理浏览器缓存,在浏览器的 DevTools 中的 Network 内 勾选 Disable cache 以禁用缓存。

  • 添加 tomcat-embed-jasper 依赖后,访问 localhost:8080/hello,页面 404。

答:设置运行主启动类的 Run/Debug Configurations 中的 Working directory 为当前模块所在目录。

参考链接:springboot 在idea多模块下 子模块的web项目用内置tomcat启动访问jsp报404

39. Boot 启动过程

39.1 SpringApplication 的构造

SpringBoot 的主启动类类似于:

1
2
3
4
5
6
@SpringBootApplication
public class BootApplication {
public static void main(String[] args) {
SpringApplication.run(BootApplication.class, args);
}
}

其中 SpringApplication#run() 方法是核心方法:

1
2
3
4
5
6
7
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);
}

最终使用 new 关键字构造了 SpringApplication 对象,然后调用了非静态 run() 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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));
this.webApplicationType = WebApplicationType.deduceFromClasspath();
this.bootstrapRegistryInitializers = new ArrayList<>(
getSpringFactoriesInstances(BootstrapRegistryInitializer.class));
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
this.mainApplicationClass = deduceMainApplicationClass();
}

构造 SpringApplication 对象时做了如下几件事:

  1. 获取 Bean Definition 源
  2. 推断应用类型
  3. 添加 ApplicationContext 初始化器
  4. 添加事件监听器
  5. 主类推断

获取 Bean Definition 源

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
@Configuration
public class A39_1 {
public static void main(String[] args) {
SpringApplication spring = new SpringApplication(A39_1.class);

// 创建并初始化 Spring 容器
ConfigurableApplicationContext context = spring.run(args);
Arrays.stream(context.getBeanDefinitionNames()).forEach(i -> {
System.out.println("name: " + i +
" 来源: " + context.getBeanFactory().getBeanDefinition(i).getResourceDescription());
});
context.close();
}

static class Bean1 {
}

static class Bean2 {
}

@Bean
public Bean2 bean2() {
return new Bean2();
}
}

运行 main() 方法后,控制台打印出错误信息:

***************************
APPLICATION FAILED TO START
***************************
Description:
Web application could not be started as there was no org.springframework.boot.web.servlet.server.ServletWebServerFactory bean defined in the context.
Action:
Check your application's dependencies for a supported servlet web server. Check the configured web application type.

这是因为添加了 spring-boot-starter-web 依赖,但 Spring 容器中并没有 ServletWebServerFactory 类型的 Bean。向容器中添加即可:

1
2
3
4
@Bean
public TomcatServletWebServerFactory servletWebServerFactory() {
return new TomcatServletWebServerFactory();
}

之后在运行 main() 方法:

name: org.springframework.context.annotation.internalConfigurationAnnotationProcessor 来源: null
name: org.springframework.context.annotation.internalAutowiredAnnotationProcessor 来源: null
name: org.springframework.context.annotation.internalCommonAnnotationProcessor 来源: null
name: org.springframework.context.event.internalEventListenerProcessor 来源: null
name: org.springframework.context.event.internalEventListenerFactory 来源: null
name: a39_1 来源: null
name: org.springframework.boot.autoconfigure.internalCachingMetadataReaderFactory 来源: null
name: bean2 来源: indi.mofan.a39.A39_1
name: servletWebServerFactory 来源: indi.mofan.a39.A39_1

来源为 null 的 Bean 是由 Spring 提供的“内置” Bean。

使用 XML 配置文件添加 Bean,并利用 setSources() 方法设置创建 ApplicationContext 的其他源:

1
2
3
4
5
public static void main(String[] args) {
SpringApplication spring = new SpringApplication(A39_1.class);
spring.setSources(Collections.singleton("classpath:b01.xml"));
// --snip--
}

再次运行 main() 方法,控制台打印的内容多了一条:

name: bean1 来源: class path resource [b01.xml]

推断应用类型

应用类型的推断在构造方法中可以看到:

1
2
3
4
5
6
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
// --snip--
// 推断应用类型
this.webApplicationType = WebApplicationType.deduceFromClasspath();
// --snip--
}

推断逻辑由 WebApplicationType 枚举中的 deduceFromClasspath() 方法完成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static WebApplicationType deduceFromClasspath() {
// ClassUtils.isPresent() 判断类路径下是否存在某个类
if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null) && !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null)
&& !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) {
// 响应式 Web 应用
return WebApplicationType.REACTIVE;
}
for (String className : SERVLET_INDICATOR_CLASSES) {
if (!ClassUtils.isPresent(className, null)) {
// 非 Web 应用
return WebApplicationType.NONE;
}
}
// Web 应用
return WebApplicationType.SERVLET;
}

利用反射调用 deduceFromClasspath() 方法:

1
2
3
4
5
6
7
8
9
10
@SneakyThrows
public static void main(String[] args) {
// --snip--

Method deduceFromClasspath = WebApplicationType.class.getDeclaredMethod("deduceFromClasspath");
deduceFromClasspath.setAccessible(true);
System.out.println("\t应用类型为: " + deduceFromClasspath.invoke(null));

// --snip--
}
	应用类型为: SERVLET

添加 ApplicationContext 初始化器

调用 SpringApplication 对象的 run() 方法时会创建 ApplicationContext,最后调用 ApplicationContextrefresh() 方法完成初始化。

在创建与初始化完成之间的一些拓展功能就由 ApplicationContext 初始化器完成。

SpringApplication 的构造方法中,添加的初始化器信息从配置文件中读取:

1
2
3
4
5
6
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
// --snip--
// 从配置文件中读取初始化器
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
// --snip--
}

也可以调用 SpringApplication 对象的 addInitializers() 方法添加自定义初始化器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@SneakyThrows
public static void main(String[] args) {
// --snip--

spring.addInitializers(applicationContext -> {
if (applicationContext instanceof GenericApplicationContext) {
GenericApplicationContext context = (GenericApplicationContext) applicationContext;
context.registerBean("bean3", Bean3.class);
}
});

// 创建并初始化 Spring 容器
ConfigurableApplicationContext context = spring.run(args);
Arrays.stream(context.getBeanDefinitionNames()).forEach(i -> {
System.out.println("name: " + i +
" 来源: " + context.getBeanFactory().getBeanDefinition(i).getResourceDescription());
});
context.close();
}

static class Bean3 {
}

运行 main() 方法后,控制台打印的 Bean 又多了一条:

name: bean3 来源: null

添加事件监听器

与添加 ApplicationContext 初始化器一样,在 SpringApplication 的构造方法中,添加的事件监听器信息从配置文件中读取:

1
2
3
4
5
6
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
// --snip--
// 从配置文件中读取事件监听器
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
// --snip--
}

可以调用 SpringApplication 对象的 addListeners() 方法添加自定义事件监听器:

1
2
3
4
5
6
7
8
@SneakyThrows
public static void main(String[] args) {
// --snip--
// 输出所有事件信息
spring.addListeners(event -> System.out.println("\t事件为: " + event));
// --snip--
context.close();
}

运行 main() 方法后,控制台打印的事件信息汇总后如下:

	事件类型为: class org.springframework.boot.context.event.ApplicationStartingEvent
	事件类型为: class org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent
	事件类型为: class org.springframework.boot.context.event.ApplicationContextInitializedEvent
	事件类型为: class org.springframework.boot.context.event.ApplicationPreparedEvent
	事件类型为: class org.springframework.boot.web.servlet.context.ServletWebServerInitializedEvent
	事件类型为: class org.springframework.context.event.ContextRefreshedEvent
	事件类型为: class org.springframework.boot.context.event.ApplicationStartedEvent
	事件类型为: class org.springframework.boot.availability.AvailabilityChangeEvent
	事件类型为: class org.springframework.boot.context.event.ApplicationReadyEvent
	事件类型为: class org.springframework.boot.availability.AvailabilityChangeEvent
	事件类型为: class org.springframework.boot.availability.AvailabilityChangeEvent
	事件类型为: class org.springframework.context.event.ContextClosedEvent

主类推断

主类推断在构造方法中可以看到:

1
2
3
4
5
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
// --snip--
// 主类推断
this.mainApplicationClass = deduceMainApplicationClass();
}

推断逻辑由 deduceMainApplicationClass() 方法完成,利用反射调用该方法:

1
2
3
4
5
6
7
8
9
10
@SneakyThrows
public static void main(String[] args) {
// --snip--

Method deduceMainApplicationClass = SpringApplication.class.getDeclaredMethod("deduceMainApplicationClass");
deduceMainApplicationClass.setAccessible(true);
System.out.println("\t主类是: " + deduceMainApplicationClass.invoke(spring));

// --snip--
}
	主类是: class indi.mofan.a39.A39_1

39.2 SpringApplication#run() 的分析

第一步:获取 SpringApplicationRunListeners

在执行 run() 方法时,首先会获取到 SpringApplicationRunListeners,它是事件发布器的组合,能够在 SpringBoot 启动的各个阶段中发布事件。

SpringApplicationRunListeners 中使用 SpringApplicationRunListener 来描述单个事件发布器,SpringApplicationRunListener 是一个接口,它有且仅有一个实现类 EventPublishingRunListener

在 SpringBoot 中,事件发布器都是在配置文件中读取,从 META-INF/spring.factories 中读取,该文件中有这样一句:

1
2
3
# Run Listeners
org.springframework.boot.SpringApplicationRunListener=\
org.springframework.boot.context.event.EventPublishingRunListener

自行实现从 META-INF/spring.factories 配置文件中读取事件发布器信息,并发布各种事件:

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
public class A39_2 {
@SneakyThrows
public static void main(String[] args) {
SpringApplication app = new SpringApplication();
app.addListeners(i -> System.out.println(i.getClass()));

// 获取时间发送器实现类名
List<String> names = SpringFactoriesLoader.loadFactoryNames(
SpringApplicationRunListener.class,
A39_2.class.getClassLoader()
);
for (String name : names) {
// System.out.println(name);
Class<?> clazz = Class.forName(name);
Constructor<?> constructor = clazz.getConstructor(SpringApplication.class, String[].class);
SpringApplicationRunListener publisher = (SpringApplicationRunListener) constructor.newInstance(app, args);

// 发布事件
DefaultBootstrapContext bootstrapContext = new DefaultBootstrapContext();
// spring boot 开始启动
publisher.starting(bootstrapContext);
// 环境信息准备完毕
publisher.environmentPrepared(bootstrapContext, new StandardEnvironment());
// 创建 spring 容器,调用初始化器之后发布此事件
GenericApplicationContext context = new GenericApplicationContext();
publisher.contextPrepared(context);
// 所有 bean definition 加载完毕
publisher.contextLoaded(context);
// spring 容器初始化完毕(调用 refresh() 方法后)
context.refresh();
publisher.started(context, null);
// spring boot 启动完毕
publisher.ready(context, null);

// 启动过程中出现异常,spring boot 启动出错
publisher.failed(context, new Exception("出错了"));
}
}
}

在 SpringBoot 启动过程中,总共发布 7 种事件。

运行 main() 方法后,控制台打印出:

class org.springframework.boot.context.event.ApplicationStartingEvent
class org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent
class org.springframework.boot.context.event.ApplicationContextInitializedEvent
class org.springframework.boot.context.event.ApplicationPreparedEvent
class org.springframework.context.event.ContextRefreshedEvent
class org.springframework.boot.context.event.ApplicationStartedEvent
class org.springframework.boot.availability.AvailabilityChangeEvent
class org.springframework.boot.context.event.ApplicationReadyEvent
class org.springframework.boot.availability.AvailabilityChangeEvent
class org.springframework.boot.context.event.ApplicationFailedEvent

但打印出的事件种类并不止 7 种,这是因为包含了其他事件发布器发布的事件,EventPublishingRunListener 发布的事件的全限定类名包含 boot.context.event,根据这个条件重新计算,恰好 7 个。

第八到十一步:完成 Spring 容器的创建

  • 第八步:创建容器。在构造 SpringApplication 时已经推断出应用的类型,使用应用类型直接创建即可。
  • 第九步:准备容器。回调在构造 SpringApplication 时添加的初始化器。
  • 第十步:加载 Bean 定义。从配置类、XML 配置文件读取 BeanDefinition,或者扫描某一包路径下的 BeanDefinition。
  • 第十一步:调用 ApplicationContextrefresh() 方法,完成 Spring 容器的创建。
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
@SneakyThrows
@SuppressWarnings("all")
public static void main(String[] args) {
SpringApplication app = new SpringApplication();
app.addInitializers(applicationContext -> System.out.println("执行初始化器增强..."));

System.out.println(">>>>>>>>>>>>>>>>>>>>>>>> 8. 创建容器");
GenericApplicationContext context = createApplicationContext(WebApplicationType.SERVLET);

System.out.println(">>>>>>>>>>>>>>>>>>>>>>>> 9. 准备容器");
for (ApplicationContextInitializer initializer : app.getInitializers()) {
initializer.initialize(context);
}

System.out.println(">>>>>>>>>>>>>>>>>>>>>>>> 10. 加载 Bean 定义");
DefaultListableBeanFactory beanFactory = context.getDefaultListableBeanFactory();
AnnotatedBeanDefinitionReader reader1 = new AnnotatedBeanDefinitionReader(beanFactory);
XmlBeanDefinitionReader reader2 = new XmlBeanDefinitionReader(beanFactory);
ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(beanFactory);

reader1.register(Config.class);
reader2.loadBeanDefinitions(new ClassPathResource("b03.xml"));
scanner.scan("indi.mofan.a39.sub");

System.out.println(">>>>>>>>>>>>>>>>>>>>>>>> 11. refresh 容器");
context.refresh();

for (String name : context.getBeanDefinitionNames()) {
System.out.println("name: " + name + " 来源: " + beanFactory.getBeanDefinition(name).getResourceDescription());
}
}

private static GenericApplicationContext createApplicationContext(WebApplicationType type) {
GenericApplicationContext context = null;
switch (type) {
case SERVLET:
context = new AnnotationConfigServletWebServerApplicationContext();
break;
case REACTIVE:
context = new AnnotationConfigReactiveWebServerApplicationContext();
break;
case NONE:
context = new AnnotationConfigApplicationContext();
break;
}
return context;
}

涉及到的配置类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static class Bean4 {

}

static class Bean5 {

}

@Configuration
static class Config {
@Bean
public Bean5 bean5() {
return new Bean5();
}

@Bean
public ServletWebServerFactory servletWebServerFactory() {
return new TomcatServletWebServerFactory();
}
}

XML 配置文件:

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="bean4" class="indi.mofan.a39.A39_3.Bean4"/>

</beans>

indi.mofan.a39.sub 包下的 Bean 信息:

1
2
3
4
5
6
7
package indi.mofan.a39.sub;

import org.springframework.stereotype.Component;

@Component
public class Bean7 {
}

运行 main() 方法后,控制台打印出的 Bean 信息:

name: org.springframework.context.annotation.internalConfigurationAnnotationProcessor 来源: null
name: org.springframework.context.annotation.internalAutowiredAnnotationProcessor 来源: null
name: org.springframework.context.annotation.internalCommonAnnotationProcessor 来源: null
name: org.springframework.context.event.internalEventListenerProcessor 来源: null
name: org.springframework.context.event.internalEventListenerFactory 来源: null
name: a39_3.Config 来源: null
name: bean4 来源: class path resource [b03.xml]
name: bean7 来源: file [D:\Code\IdeaCode\advanced-spring\boot\target\classes\indi\mofan\a39\sub\Bean7.class]
name: org.springframework.boot.autoconfigure.internalCachingMetadataReaderFactory 来源: null
name: bean5 来源: indi.mofan.a39.A39_3$Config
name: servletWebServerFactory 来源: indi.mofan.a39.A39_3$Config

第二步:封装启动 args

调用 DefaultApplicationArguments 的构造方法,传入 args 即可:

1
2
3
4
5
6
7
8
@SneakyThrows
@SuppressWarnings("all")
public static void main(String[] args) {
// --snip--
System.out.println(">>>>>>>>>>>>>>>>>>>>>>>> 2. 封装启动 args");
DefaultApplicationArguments arguments = new DefaultApplicationArguments(args);
// --snip--
}

第十二步:执行 Runner

在 SpringBoot 启动成功后,可以执行一些 Runner,进行一些预处理或测试。Runner 有两种,分别是 CommandLineRunnerApplicationRunner

1
2
3
4
5
6
7
8
9
@FunctionalInterface
public interface CommandLineRunner {
void run(String... args) throws Exception;
}

@FunctionalInterface
public interface ApplicationRunner {
void run(ApplicationArguments args) throws Exception;
}

它们都是函数式接口,内部的抽象方法长得也很像,只不过:

  • CommandLineRunner 直接接收启动参数;
  • ApplicationRunner 则是接收封装后的 ApplicationArguments,即 第二步 封装的对象。

在配置类中添加这两种类型的 Bean:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Bean
public CommandLineRunner commandLineRunner() {
return args -> System.out.println("commandLineRunner()..." + Arrays.toString(args));
}

@Bean
public ApplicationRunner applicationRunner() {
return args -> {
// 获取原始参数
System.out.println("applicationRunner()..."
+ Arrays.toString(args.getSourceArgs()));
// 获取选项名称,参数中带有 `--` 的参数
System.out.println(args.getOptionNames());
// 获取选项值
System.out.println(args.getOptionValues("server.port"));
// 获取非选项参数
System.out.println(args.getNonOptionArgs());
};
}

执行 Runner

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@SneakyThrows
@SuppressWarnings("all")
public static void main(String[] args) {
// --snip--

System.out.println(">>>>>>>>>>>>>>>>>>>>>>>> 2. 封装启动 args");
DefaultApplicationArguments arguments = new DefaultApplicationArguments(args);

// --snip--

System.out.println(">>>>>>>>>>>>>>>>>>>>>>>> 12. 执行 runner");
for (CommandLineRunner runner : context.getBeansOfType(CommandLineRunner.class).values()) {
runner.run(args);
}

for (ApplicationRunner runner : context.getBeansOfType(ApplicationRunner.class).values()) {
runner.run(arguments);
}
}

运行 main() 方法时,需要添加程序参数 --server.port=8080 debug

执行Runner添加程序参数

>>>>>>>>>>>>>>>>>>>>>>>> 12. 执行 runner
commandLineRunner()...[--server.port=8080, debug]
applicationRunner()...[--server.port=8080, debug]
[server.port]
[8080]
[debug]

第三步:准备 Environment 添加命令行参数

Environment 即环境对象,是对配置信息的抽象,配置信息的来源有多种,比如:系统环境变量、properties 配置文件、YAML 配置文件等等。

SpringBoot 提供了名为 ApplicationEnvironment 的类表示环境对象,它是 Spring 中 StandardEnvironment 环境对象的子类。

ApplicationEnvironment的类图

默认情况下,创建的 ApplicationEnvironment 对象中配置信息的来源只有两个:

  • 系统属性
  • 系统变量
1
2
3
4
5
6
7
8
9
10
11
12
package org.springframework.boot;

/**
* @author mofan
* @date 2023/1/28 12:12
*/
public class Step3 {
public static void main(String[] args) {
ApplicationEnvironment env = new ApplicationEnvironment();
env.getPropertySources().forEach(System.out::println);
}
}
PropertiesPropertySource {name='systemProperties'}
SystemEnvironmentPropertySource {name='systemEnvironment'}    

针对相同名称的配置信息,按照来源的先后顺序获取。

获取 JAVA_HOME 的配置信息:

1
2
3
4
public static void main(String[] args) {
ApplicationEnvironment env = new ApplicationEnvironment();
System.out.println(env.getProperty("JAVA_HOME"));
}
D:\environment\JDK1.8

由于 PropertiesPropertySource 中并不存在名为 JAVA_HOME 的配置信息,因此从系统环境变量 SystemEnvironmentPropertySource 中获取 JAVA_HOME 的配置信息。

在 IDEA 的 Run/Debug Configurations 中的 VM options 添加 -DJAVA_HOME=abc,使得 PropertiesPropertySource 中存在名为 JAVA_HOME 的配置信息:

添加-DJAVA_HOME=abc配置信息

之后再运行 main() 方法,控制台打印出:

abc

如果想从配置文件 application.properties 中读取配置信息,可以添加配置信息的来源。配置文件的优先级最低,添加来源时调用 addLast() 方法:

1
2
3
4
5
6
7
8
@SneakyThrows
public static void main(String[] args) {
ApplicationEnvironment env = new ApplicationEnvironment();
env.getPropertySources().addLast(new ResourcePropertySource(new ClassPathResource("application.properties")));
env.getPropertySources().forEach(System.out::println);

System.out.println(env.getProperty("author.name"));
}
1
author.name="mofan"
PropertiesPropertySource {name='systemProperties'}
SystemEnvironmentPropertySource {name='systemEnvironment'}
ResourcePropertySource {name='class path resource [application.properties]'}
"mofan"

而在 SpringBoot 中,这里 添加 SimpleCommandLinePropertySource,并且它的优先级最高,使用 addFirst() 方法添加:

1
2
3
4
5
6
7
8
9
@SneakyThrows
public static void main(String[] args) {
ApplicationEnvironment env = new ApplicationEnvironment();
env.getPropertySources().addLast(new ResourcePropertySource(new ClassPathResource("application.properties")));
env.getPropertySources().addFirst(new SimpleCommandLinePropertySource(args));
env.getPropertySources().forEach(System.out::println);

System.out.println(env.getProperty("author.name"));
}

运行 main() 方法前,需要添加程序参数 --author.name=默烦

添加程序参数--author.name=默烦

SimpleCommandLinePropertySource {name='commandLineArgs'}
PropertiesPropertySource {name='systemProperties'}
SystemEnvironmentPropertySource {name='systemEnvironment'}
ResourcePropertySource {name='class path resource [application.properties]'}
默烦

第四步:添加 ConfigurationPropertySources

有一 step4.properties 文件,其内容如下:

1
2
3
user.first-name=George
user.middle_name=Walker
user.lastName=Bush

尝试读取文件中的内容:

1
2
3
4
5
6
7
8
9
10
11
@SneakyThrows
public static void main(String[] args) {
ApplicationEnvironment env = new ApplicationEnvironment();
env.getPropertySources().addLast(
new ResourcePropertySource("step4", new ClassPathResource("step4.properties"))
);
env.getPropertySources().forEach(System.out::println);
System.out.println(env.getProperty("user.first-name"));
System.out.println(env.getProperty("user.middle-name"));
System.out.println(env.getProperty("user.last-name"));
}

step4.properties 文件中配置信息的 key 是 user.middle_name,但在读取时,使用的是 user.middle-name;还有 user.lastName 的 key,但读取时使用 user.last-name。能读取成功吗?

PropertiesPropertySource {name='systemProperties'}
SystemEnvironmentPropertySource {name='systemEnvironment'}
ResourcePropertySource {name='step4'}
George
null
null

显然是不行的,为了能读取成功,需要实现 松散绑定,添加 ConfigurationPropertySources

1
2
3
4
5
6
@SneakyThrows
public static void main(String[] args) {
// --snip--
ConfigurationPropertySources.attach(env);
// --snip--
}
ConfigurationPropertySourcesPropertySource {name='configurationProperties'}
PropertiesPropertySource {name='systemProperties'}
SystemEnvironmentPropertySource {name='systemEnvironment'}
ResourcePropertySource {name='step4'}
George
Walker
Bush

第五步:使用 EnvironmentPostProcessorApplicationListener 进行环境对象后置处理

在第三步中 添加 SimpleCommandLinePropertySource,读取 properties、YAML 配置文件的源就是在第五步中添加的。

完成这样功能需要使用到 EnvironmentPostProcessor,其具体实现是 ConfigDataEnvironmentPostProcessor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void main(String[] args) {
SpringApplication app = new SpringApplication();
ApplicationEnvironment env = new ApplicationEnvironment();

System.out.println(">>>>>>>>>>>>>>>>>>>>>>>> 增强前");
env.getPropertySources().forEach(System.out::println);
ConfigDataEnvironmentPostProcessor processor1 = new ConfigDataEnvironmentPostProcessor(
new DeferredLogs(), new DefaultBootstrapContext()
);
processor1.postProcessEnvironment(env, app);

System.out.println(">>>>>>>>>>>>>>>>>>>>>>>> 增强后");
env.getPropertySources().forEach(System.out::println);
System.out.println(env.getProperty("author.name"));

RandomValuePropertySourceEnvironmentPostProcessor processor2 =
new RandomValuePropertySourceEnvironmentPostProcessor(new DeferredLog());
processor2.postProcessEnvironment(env, app);
}
>>>>>>>>>>>>>>>>>>>>>>>> 增强前
PropertiesPropertySource {name='systemProperties'}
SystemEnvironmentPropertySource {name='systemEnvironment'}
>>>>>>>>>>>>>>>>>>>>>>>> 增强后
PropertiesPropertySource {name='systemProperties'}
SystemEnvironmentPropertySource {name='systemEnvironment'}
OriginTrackedMapPropertySource {name='Config resource 'class path resource [application.properties]' via location 'optional:classpath:/''}
"mofan"

EnvironmentPostProcessor 还有一个有趣的实现:RandomValuePropertySourceEnvironmentPostProcessor,该实现提供了随机值的生成。

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
// --snip--

System.out.println(">>>>>>>>>>>>>>>>>>>>>>>> 再次增强后");
env.getPropertySources().forEach(System.out::println);
System.out.println(env.getProperty("random.string"));
System.out.println(env.getProperty("random.int"));
System.out.println(env.getProperty("random.uuid"));
}
>>>>>>>>>>>>>>>>>>>>>>>> 再次增强后
PropertiesPropertySource {name='systemProperties'}
SystemEnvironmentPropertySource {name='systemEnvironment'}
RandomValuePropertySource {name='random'}
OriginTrackedMapPropertySource {name='Config resource 'class path resource [application.properties]' via location 'optional:classpath:/''}
5ef4038a709215938cbd3e1c031f66dd
1481116109
18548e0b-8bad-458b-b38e-bf793aa24ced

在 SpringBoot 中的实现是不会采取上述示例代码的方式来添加后置处理器,同样会从 META-INF/spring.factories 配置文件中读取并初始化后置处理器:

1
2
3
4
5
6
7
8
# Environment Post Processors
org.springframework.boot.env.EnvironmentPostProcessor=\
org.springframework.boot.cloud.CloudFoundryVcapEnvironmentPostProcessor,\
org.springframework.boot.context.config.ConfigDataEnvironmentPostProcessor,\
org.springframework.boot.env.RandomValuePropertySourceEnvironmentPostProcessor,\
org.springframework.boot.env.SpringApplicationJsonEnvironmentPostProcessor,\
org.springframework.boot.env.SystemEnvironmentPropertySourceEnvironmentPostProcessor,\
org.springframework.boot.reactor.DebugAgentEnvironmentPostProcessor

SpringBoot 中读取 META-INF/spring.factories 配置文件初始化环境后置处理器,再执行处理逻辑的功能由 EnvironmentPostProcessorApplicationListener 完成。它是一个事件监听器,同样是在 META-INF/spring.factories 配置文件中读取并初始化的:

1
2
3
4
5
6
7
8
org.springframework.context.ApplicationListener=\
org.springframework.boot.ClearCachesApplicationListener,\
org.springframework.boot.builder.ParentContextCloserApplicationListener,\
org.springframework.boot.context.FileEncodingApplicationListener,\
org.springframework.boot.context.config.AnsiOutputApplicationListener,\
org.springframework.boot.context.config.DelegatingApplicationListener,\
org.springframework.boot.context.logging.LoggingApplicationListener,\
org.springframework.boot.env.EnvironmentPostProcessorApplicationListener

要想该监听器成功监听到事件,需要在第五步中发布一个事件,而事件的发布由第一步获取的事件发布器完成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) {
SpringApplication app = new SpringApplication();
app.addListeners(new EnvironmentPostProcessorApplicationListener());
ApplicationEnvironment env = new ApplicationEnvironment();

List<String> names = SpringFactoriesLoader.loadFactoryNames(EnvironmentPostProcessor.class, Step5.class.getClassLoader());
names.forEach(System.out::println);

EventPublishingRunListener publisher = new EventPublishingRunListener(app, args);
System.out.println(">>>>>>>>>>>>>>>>>>>>>>>> 增强前");
env.getPropertySources().forEach(System.out::println);
publisher.environmentPrepared(new DefaultBootstrapContext(), env);
System.out.println(">>>>>>>>>>>>>>>>>>>>>>>> 增强后");
env.getPropertySources().forEach(System.out::println);
}
org.springframework.boot.cloud.CloudFoundryVcapEnvironmentPostProcessor
org.springframework.boot.context.config.ConfigDataEnvironmentPostProcessor
org.springframework.boot.env.RandomValuePropertySourceEnvironmentPostProcessor
org.springframework.boot.env.SpringApplicationJsonEnvironmentPostProcessor
org.springframework.boot.env.SystemEnvironmentPropertySourceEnvironmentPostProcessor
org.springframework.boot.reactor.DebugAgentEnvironmentPostProcessor
org.springframework.boot.autoconfigure.integration.IntegrationPropertiesEnvironmentPostProcessor
>>>>>>>>>>>>>>>>>>>>>>>> 增强前
PropertiesPropertySource {name='systemProperties'}
SystemEnvironmentPropertySource {name='systemEnvironment'}
>>>>>>>>>>>>>>>>>>>>>>>> 增强后
PropertiesPropertySource {name='systemProperties'}
OriginAwareSystemEnvironmentPropertySource {name='systemEnvironment'}
RandomValuePropertySource {name='random'}
OriginTrackedMapPropertySource {name='Config resource 'class path resource [application.properties]' via location 'optional:classpath:/''}

配置文件中 EnvironmentPostProcessor 的实现有很多,但根据上述打印出的信息,生效的并不多,是否生效与项目的依赖配置有关。

第六步:绑定 spring.main 前缀的配置信息到 SpringApplication 对象

使用 @ConfigurationProperties 注解可以指定一个前缀,SpringBoot 将根据指定的前缀和属性名称在配置文件中寻找对应的信息并完成注入,其底层是利用 Binder 实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@SneakyThrows
public static void main(String[] args) {
SpringApplication app = new SpringApplication();
ApplicationEnvironment env = new ApplicationEnvironment();
env.getPropertySources().addLast(
new ResourcePropertySource("step4", new ClassPathResource("step4.properties"))
);

User user = Binder.get(env).bind("user", User.class).get();
System.out.println(user);

User existUser = new User();
Binder.get(env).bind("user", Bindable.ofInstance(existUser));
System.out.println(existUser);
}

@Getter
@Setter
@ToString
static class User {
private String firstName;
private String middleName;
private String lastName;
}
Step6.User(firstName=George, middleName=Walker, lastName=Bush)
Step6.User(firstName=George, middleName=Walker, lastName=Bush)

在第六步中,绑定 spring.main 前缀的配置信息到 SpringApplication 对象也是利用了 Binder

假设 step6.properties 配置文件的信息如下:

1
2
spring.main.banner-mode=off
spring.main.lazy-initialization=true

绑定 spring.main 开头的配置信息到 SpringApplication 对象中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@SneakyThrows
public static void main(String[] args) {
SpringApplication app = new SpringApplication();
ApplicationEnvironment env = new ApplicationEnvironment();
env.getPropertySources().addLast(
new ResourcePropertySource("step6", new ClassPathResource("step6.properties"))
);

Class<? extends SpringApplication> clazz = app.getClass();
Field bannerMode = clazz.getDeclaredField("bannerMode");
bannerMode.setAccessible(true);
Field lazyInitialization = clazz.getDeclaredField("lazyInitialization");
lazyInitialization.setAccessible(true);
System.out.println(bannerMode.get(app));
System.out.println(lazyInitialization.get(app));
Binder.get(env).bind("spring.main", Bindable.ofInstance(app));
System.out.println(bannerMode.get(app));
System.out.println(lazyInitialization.get(app));
}
CONSOLE
false
OFF
true

第七步:打印 Banner

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
ApplicationEnvironment env = new ApplicationEnvironment();
SpringApplicationBannerPrinter printer = new SpringApplicationBannerPrinter(
new DefaultResourceLoader(),
new SpringBootBanner()
);

printer.print(env, Step7.class, System.out);
}

除此之外还可以自定义文字和图片 Banner,文字 Banner 的文件类型需要是 txt,图片 Banner 的文件类型需要是 gif

文字 Banner:

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) {
// --snip--

// 测试文字 banner
env.getPropertySources().addLast(new MapPropertySource(
"custom",
Collections.singletonMap("spring.banner.location", "banner1.txt")
));
printer.print(env, Step7.class, System.out);
}

文字 Banner 可以从 网站 上自定义。

图片 Banner:

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) {
// --snip--

// 测试图片 banner
env.getPropertySources().addLast(new MapPropertySource(
"custom",
Collections.singletonMap("spring.banner.image.location", "banner2.gif")
));
printer.print(env, Step7.class, System.out);
}

获取 Spring 或 SpringBoot 的版本号可以使用:

1
2
System.out.println("SpringBoot: " + SpringBootVersion.getVersion());
System.out.println("Spring: " + SpringVersion.getVersion());

步骤总结

  1. 得到 SpringApplicationRunListeners 事件发布器

    • 发布 Application Starting 事件 1️⃣
  2. 封装启动 args

  3. 准备 Environment 添加命令行参数

  4. ConfigurationPropertySources 处理

    • 发布 Application Environment 已准备事件 2️⃣
  5. 通过 EnvironmentPostProcessorApplicationListener 进行 env 后处理

    • application.propertiesStandardConfigDataLocationResolver 解析
    • spring.application.json
  6. 绑定 spring.mainSpringApplication 对象

  7. 打印 Banner

  8. 创建容器

  9. 准备容器

    • 发布 Application Context 已初始化事件 3️⃣
  10. 加载 Bean 定义

    • 发布 Application Prepared 事件 4️⃣
  11. refresh 容器

    • 发布 Application Started 事件 5️⃣
  12. 执行 Runner

    • 发布 Application Ready 事件 6️⃣

    • 这其中有异常,发布 Application Failed 事件 7️⃣

40. Tomcat 内嵌容器

Tomcat 基本结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Server
└───Service
├───Connector (协议, 端口)
└───Engine
└───Host(虚拟主机 localhost)
├───Context1 (应用 1, 可以设置虚拟路径, / 即 url 起始路径; 项目磁盘路径, 即 docBase)
│ │ index.html
│ └───WEB-INF
│ │ web.xml (servlet, filter, listener) 3.0
│ ├───classes (servlet, controller, service ...)
│ ├───jsp
│ └───lib (第三方 jar 包)
└───Context2 (应用 2)
│ index.html
└───WEB-INF
web.xml

40.1 内嵌 Tomcat 的使用

内嵌 Tomcat 的使用分为 6 步:

  1. 创建 Tomcat
  2. 创建项目文件夹,即 docBase 文件夹
  3. 创建 Tomcat 项目,在 Tomcat 中称为 Context
  4. 编程添加 Servlet
  5. 启动 Tomcat
  6. 创建连接器,设置监听端口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@SneakyThrows
public static void main(String[] args) {
// 1. 创建 Tomcat
Tomcat tomcat = new Tomcat();
tomcat.setBaseDir("tomcat");
// 2. 创建项目文件夹,即 docBase 文件夹
File docBase = Files.createTempDirectory("boot.").toFile();
docBase.deleteOnExit();
// 3. 创建 tomcat 项目,在 tomcat 中称为 Context
Context context = tomcat.addContext("", docBase.getAbsolutePath());
// 4. 编程添加 Servlet
context.addServletContainerInitializer((set, servletContext) -> {
HelloServlet servlet = new HelloServlet();
// 还要设置访问 Servlet 的路径
servletContext.addServlet("hello", servlet).addMapping("/hello");
}, Collections.emptySet());
// 5. 启动 tomcat
tomcat.start();
// 6. 创建连接器,设置监听端口
Connector connector = new Connector(new Http11Nio2Protocol());
connector.setPort(8080);
tomcat.setConnector(connector);
}

自行实现的 Servlet 需要继承 HttpServlet,并重写 doGet() 方法:

1
2
3
4
5
6
7
8
9
10
public class HelloServlet extends HttpServlet {
private static final long serialVersionUID = 8117441197359625079L;

@Override
protected void doGet(HttpServletRequest req,
HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html;charset=utf-8");
resp.getWriter().print("<h3>hello</h3>");
}
}

运行 main() 方法后,在浏览器访问 localhost:8080/hello,页面显示 hello

40.2 与 Spring 整合

首先肯定需要一个 Spring 容器,选择不支持内嵌 Tomcat 的 Spring 容器,使其使用前文中的 Tomcat:

1
2
3
4
5
6
7
public static WebApplicationContext getApplicationContext() {
// 使用不支持内嵌 Tomcat 的 Spring 容器
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
context.register(Config.class);
context.refresh();
return context;
}

容器中注册了 Config Bean:

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
@Configuration
static class Config {
@Bean
public DispatcherServletRegistrationBean registrationBean(DispatcherServlet dispatcherServlet) {
return new DispatcherServletRegistrationBean(dispatcherServlet, "/");
}

@Bean
public DispatcherServlet dispatcherServlet(WebApplicationContext applicationContext) {
/*
* 必须为 DispatcherServlet 提供 AnnotationConfigWebApplicationContext,
* 否则会选择 XmlWebApplicationContext 实现
*/
return new DispatcherServlet(applicationContext);
}

@Bean
public RequestMappingHandlerAdapter requestMappingHandlerAdapter() {
RequestMappingHandlerAdapter handlerAdapter = new RequestMappingHandlerAdapter();
handlerAdapter.setMessageConverters(Collections.singletonList(new MappingJackson2HttpMessageConverter()));
return handlerAdapter;
}

@RestController
static class MyController {
@GetMapping("hello2")
public Map<String,Object> hello() {
return Collections.singletonMap("hello2", "hello2, spring!");
}
}
}

Tomcat 在添加 Servlet 时,添加 DispatcherServlet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@SneakyThrows
public static void main(String[] args) {
// --snip--

WebApplicationContext springContext = getApplicationContext();

// 4. 编程添加 Servlet
context.addServletContainerInitializer((set, servletContext) -> {
HelloServlet servlet = new HelloServlet();
// 还要设置访问 Servlet 的路径
servletContext.addServlet("hello", servlet).addMapping("/hello");

DispatcherServlet dispatcherServlet = springContext.getBean(DispatcherServlet.class);
servletContext.addServlet("dispatcherServlet", dispatcherServlet).addMapping("/");
}, Collections.emptySet());

// --snip--
}

运行 main() 方法,在浏览器中访问 localhost:8080/hello2,页面上显示:

{"hello2":"hello2, spring!"}

添加 Servlet 时只添加了一个 DispatcherServlet,但 Spring 容器中可能存在多个 Servlet,这些 Servlet 也应该被添加,因此可以获取 ServletRegistrationBean 类型的 Bean 并执行 `` 方法,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@SneakyThrows
public static void main(String[] args) {
// --snip--

WebApplicationContext springContext = getApplicationContext();

// 4. 编程添加 Servlet
context.addServletContainerInitializer((set, servletContext) -> {
HelloServlet servlet = new HelloServlet();
// 还要设置访问 Servlet 的路径
servletContext.addServlet("hello", servlet).addMapping("/hello");

// Spring 容器中可能存在多个 Servlet
for (ServletRegistrationBean registrationBean : springContext.getBeansOfType(ServletRegistrationBean.class).values()) {
registrationBean.onStartup(servletContext);
}
}, Collections.emptySet());

// --snip--
}

运行 main() 方法,在浏览器中访问 localhost:8080/hello2,页面显示同样的内容。

41. 自动配置

41.1 自动配置类原理

有以下四个类:

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
/**
* 模拟第三方配置类
*/
static class AutoConfiguration1 {
@Bean
public Bean1 bean1() {
return new Bean1();
}
}

@ToString
@NoArgsConstructor
@AllArgsConstructor
static class Bean1 {
private String name;
}

/**
* 模拟第三方配置类
*/
static class AutoConfiguration2 {
@Bean
public Bean2 bean2() {
return new Bean2();
}
}

static class Bean2 {

}

其中 AutoConfiguration1AutoConfiguration2 用来模拟第三方配置类,注意它们并没有被 @Configuration 注解标记,因此在未进行其他操作时,不会被添加到 Spring 容器中。

然后编写自己的配置类,使用 @Import 注解将第三方配置类添加到 Spring 容器中:

1
2
3
4
@Configuration
@Import({AutoConfiguration1.class, AutoConfiguration2.class})
static class Config {
}
1
2
3
4
5
6
7
8
public static void main(String[] args) {
GenericApplicationContext context = new GenericApplicationContext();
context.registerBean("config", Config.class);
context.registerBean(ConfigurationClassPostProcessor.class);
context.refresh();

Arrays.stream(context.getBeanDefinitionNames()).forEach(System.out::println);
}

运行 main() 方法后,控制台打印出:

config
org.springframework.context.annotation.ConfigurationClassPostProcessor
indi.mofan.a41.A41$AutoConfiguration1
bean1
indi.mofan.a41.A41$AutoConfiguration2
bean2

如果有多个第三方配置类,难不成到一个个地导入?

可以使用导入选择器 ImportSelector,重写 selectImports() 方法,返回需要自动装配的 Bean 的全限定类名数组:

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
@Import(MyImportSelector.class)
static class Config {

}

static class MyImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[]{AutoConfiguration1.class.getName(), AutoConfiguration2.class.getName()};
}
}

但这样的方式相比最初的方式并没有本质区别,甚至更麻烦,还多了一个类。如果 selectImports() 方法返回的全限定类名可以从文件中读取,就更方便了。

在当前项目的类路径下创建 META-INF/spring.factories 文件,约定一个 key,对应的 value 即为需要指定装配的 Bean:

1
2
3
4
# 内部类作为 key 时,最后以 $ 符号分割
indi.mofan.a41.A41$MyImportSelector=\
indi.mofan.a41.A41.AutoConfiguration1, \
indi.mofan.a41.A41.AutoConfiguration2

修改 selectImports() 方法实现逻辑:

1
2
3
4
5
6
7
static class MyImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
List<String> names = SpringFactoriesLoader.loadFactoryNames(MyImportSelector.class, null);
return names.toArray(new String[0]);
}
}

运行 main() 方法后,控制台打印出同样的结果。

SpringFactoriesLoader.loadFactoryNames() 不仅只扫描当前项目类型路径下的 META-INF/spring.factories 文件,而是会扫描包括 Jar 包里类路径下的 META-INF/spring.factories 文件。

针对 SpringBoot 来说,自动装配的 Bean 使用如下语句加载:

1
SpringFactoriesLoader.loadFactoryNames(EnableAutoConfiguration.class, null);

SpringBoot 2.7.0 及其以后版本的自动装配

在 SpringBoot 2.7.0 及其以后的版本中,SpringBoot 不再通过读取 META-INF/spring.factories 文件中 key 为 org.springframework.boot.autoconfigure.EnableAutoConfiguration 的 values 来实现自动装配。

为了更贴合 SPI 机制,SpringBoot 将读取 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件中的内容,该文件中每一行都表示需要自动装配的 Bean 的全限定类名,可以使用 # 作为注释。其加载方式使用:

1
ImportCandidates.load(AutoConfiguration.class, getBeanClassLoader());

其中 AutoConfiguration 是一个注解,它的全限定类名为 org.springframework.boot.autoconfigure.AutoConfiguration

也就是说可以自定义一个注解,创建 META-INF/spring/full-qualified-annotation-name.imports 文件,在文件里声明需要自动装配的类:

1
2
3
4
5
6
package indi.mofan.a41;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAutoConfiguration {
}
1
2
3
4
5
6
package indi.mofan.a41;

public class A41 {
static class Bean3 {
}
}

创建 META-INF/spring/indi.mofan.a41.MyAutoConfiguration.imports 文件:

1
indi.mofan.a41.A41$Bean3

修改 selectImports() 方法实现逻辑:

1
2
3
4
5
6
7
8
9
static class MyImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
List<String> names = new ArrayList<>(SpringFactoriesLoader.loadFactoryNames(MyImportSelector.class, null));
// 读取新版自动装配文件
ImportCandidates.load(MyAutoConfiguration.class, null).forEach(names::add);
return names.toArray(new String[0]);
}
}

运行 main() 方法后,Spring 容器中的 Bean 多了 一个:

indi.mofan.a41.A41$Bean3

定义了冲突的 Bean

第三方装配了 Bean1

1
2
3
4
5
6
static class AutoConfiguration1 {
@Bean
public Bean1 bean1() {
return new Bean1("第三方");
}
}

用户又自行定义了 Bean1

1
2
3
4
5
6
7
8
@Configuration
@Import(MyImportSelector.class)
static class Config {
@Bean
public Bean1 bean1() {
return new Bean1("本项目");
}
}

修改测试的 main() 方法:

1
2
3
4
5
6
public static void main(String[] args) {
// --snip--

System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>");
System.out.println(context.getBean(Bean1.class));
}

最终谁会生效呢?

>>>>>>>>>>>>>>>>>>>>>>>>>>>
A41.Bean1(name=本项目)

用户自行定义的 Bean 生效了,这是因为:@Import 导入的 Bean 先于配置类中 @Bean 定义的 Bean 执行,后者覆盖前者,使得用户自定义的 Bean 生效。

但在 SpringBoot 中不是这样的,当后续添加的 Bean 想覆盖先前添加的 Bean,会出现错误。模拟 SpringBoot 的设置:

1
2
3
4
5
6
7
public static void main(String[] args) {
GenericApplicationContext context = new GenericApplicationContext();
// 默认是 true,SpringBoot 修改为 false,使得无法进行覆盖
context.getDefaultListableBeanFactory().setAllowBeanDefinitionOverriding(false);

// --snip--
}
Exception in thread "main" org.springframework.beans.factory.support.BeanDefinitionOverrideException: Invalid bean definition with name 'bean1' defined in indi.mofan.a41.A41$Config: Cannot register bean definition [Root bean: class [null]; scope=; abstract=false; lazyInit=null; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=config; factoryMethodName=bean1; initMethodName=null; destroyMethodName=(inferred); defined in indi.mofan.a41.A41$Config] for bean 'bean1': There is already [Root bean: class [null]; scope=; abstract=false; lazyInit=null; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=indi.mofan.a41.A41$AutoConfiguration1; factoryMethodName=bean1; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [indi/mofan/a41/A41$AutoConfiguration1.class]] bound.

那这样是合理的吗?

显然不是。比如 SpringBoot 默认的数据连接池是 Hikari,如果用户想换成 Druid,岂不是做不到?

实际情况下是能做到的,这又是怎么做到的呢?

首先需要使用户的配置类中定义的 Bean 先于 @Import 导入的 Bean 添加到 Spring 容器中,只需将选择器 MyImportSelector 实现的 ImportSelector 接口更换成其子接口 DeferredImportSelector 即可:

1
2
3
static class MyImportSelector implements DeferredImportSelector {
// --snip--
}

再次运行 main() 方法:

Exception in thread "main" org.springframework.beans.factory.support.BeanDefinitionOverrideException: Invalid bean definition with name 'bean1' defined in class path resource [indi/mofan/a41/A41$AutoConfiguration1.class]: Cannot register bean definition [Root bean: class [null]; scope=; abstract=false; lazyInit=null; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=indi.mofan.a41.A41$AutoConfiguration1; factoryMethodName=bean1; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [indi/mofan/a41/A41$AutoConfiguration1.class]] for bean 'bean1': There is already [Root bean: class [null]; scope=; abstract=false; lazyInit=null; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=config; factoryMethodName=bean1; initMethodName=null; destroyMethodName=(inferred); defined in indi.mofan.a41.A41$Config] bound.

尽管还是出现了异常,但异常信息中显示的是在配置类定义的 Bean 已存在,第三方装配的 Bean 无法再添加,这表明 Bean 的添加顺序修改成功。

最后在第三方定义的 Bean 上添加 @ConditionalOnMissingBean 注解,表示容器中存在同名的 Bean 时忽略该 Bean 的添加:

1
2
3
4
5
6
7
static class AutoConfiguration1 {
@Bean
@ConditionalOnMissingBean
public Bean1 bean1() {
return new Bean1("第三方");
}
}

再次运行 main() 方法,不再出现异常:

>>>>>>>>>>>>>>>>>>>>>>>>>>>
A41.Bean1(name=本项目)

41.2 Aop 自动配置

确保当前模块下已导入:

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

使用 AopAutoConfiguration 自动装配与 AOP 相关的 Bean:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class TestAopAuto {
public static void main(String[] args) {
GenericApplicationContext context = new GenericApplicationContext();
// 注册常用后置处理器
AnnotationConfigUtils.registerAnnotationConfigProcessors(context.getDefaultListableBeanFactory());
context.registerBean(Config.class);
context.refresh();
Arrays.stream(context.getBeanDefinitionNames()).forEach(System.out::println);
}

@Configuration
@Import(MyImportSelector.class)
static class Config {
}

static class MyImportSelector implements DeferredImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[]{AopAutoConfiguration.class.getName()};
}
}
}
org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
indi.mofan.a41.TestAopAuto$Config
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration$AspectJAutoProxyingConfiguration$CglibAutoProxyConfiguration
org.springframework.aop.config.internalAutoProxyCreator
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration$AspectJAutoProxyingConfiguration
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration

indi.mofan.a41.TestAopAuto$Config 为分割线,上方是添加的一些后置处理器,下方就是 AOP 自动装配添加的 Bean。

在配置类 AopAutoConfiguration 中,使用注解判断配置类是否生效。首先是最外层的 AopAutoConfiguration

1
2
3
4
5
@AutoConfiguration
@ConditionalOnProperty(prefix = "spring.aop", name = "auto", havingValue = "true", matchIfMissing = true)
public class AopAutoConfiguration {
// --snip--
}

根据 @ConditionalOnProperty 注解配置的信息:如果配置文件中存在 前缀spring.aop名称auto 的 key,并且其对应的 value 是 true 时,配置类 AopAutoConfiguration 生效;如果配置文件中未显式配置,该配置类也生效。

不使用配置文件,使用 StandardEnvironment 指定 spring.aop.auto 的值为 false

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) {
GenericApplicationContext context = new GenericApplicationContext();
StandardEnvironment env = new StandardEnvironment();
env.getPropertySources().addLast(
new SimpleCommandLinePropertySource("--spring.aop.auto=false")
);
context.setEnvironment(env);

// --snip--
}
org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
indi.mofan.a41.TestAopAuto$Config

如果 spring.aop.auto 的值是 true,又会成功添加上 AOP 自动装配的 Bean。

再看 AopAutoConfiguration 的内部类:

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(Advice.class)
static class AspectJAutoProxyingConfiguration {
// --snip--
}

@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingClass("org.aspectj.weaver.Advice")
@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true", matchIfMissing = true)
static class ClassProxyingConfiguration {
// --snip--
}

其内部存在两个类:AspectJAutoProxyingConfigurationClassProxyingConfiguration

使用了 @ConditionalOnClass 注解判断 Advice.class 存在时,AspectJAutoProxyingConfiguration 生效;使用 @ConditionalOnMissingClass 注解判断 org.aspectj.weaver.Advice 不存在时,ClassProxyingConfiguration 生效。

由于先前导入了 spring-boot-starter-aop 依赖,Advice.class 是存在的,AspectJAutoProxyingConfiguration 将生效。

AspectJAutoProxyingConfiguration 内部又有两个配置类:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration(proxyBeanMethods = false)
@EnableAspectJAutoProxy(proxyTargetClass = false)
@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false")
static class JdkDynamicAutoProxyConfiguration {

}

@Configuration(proxyBeanMethods = false)
@EnableAspectJAutoProxy(proxyTargetClass = true)
@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true", matchIfMissing = true)
static class CglibAutoProxyConfiguration {

}

这两个配置类通过使用 @ConditionalOnProperty 注解判断配置文件中是否存在 spring.aop.proxy-target-class 配置来让对应的配置类生效。

由于并未显式配置,因此 CglibAutoProxyConfiguration 将生效。

无论哪个配置类生效,它们都被 @EnableAspectJAutoProxy 标记,这个注解相当于是添加了些配置的 @Import 注解:

1
2
3
4
5
6
7
8
9
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({AspectJAutoProxyRegistrar.class})
public @interface EnableAspectJAutoProxy {
boolean proxyTargetClass() default false;

boolean exposeProxy() default false;
}

向 Spring 容器中添加 AspectJAutoProxyRegistrar 类型的 Bean。

AspectJAutoProxyRegistrar 实现了 ImportBeanDefinitionRegistrar 接口,可以使用编程的方式来注册一些 Bean:

1
2
3
4
5
6
class AspectJAutoProxyRegistrar implements ImportBeanDefinitionRegistrar {
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry);
// --snip--
}
}

AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary() 方法是注册 Bean 的主要逻辑:

1
2
3
4
5
6
7
8
9
@Nullable
public static BeanDefinition registerAspectJAnnotationAutoProxyCreatorIfNecessary(BeanDefinitionRegistry registry) {
return registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry, (Object)null);
}

@Nullable
public static BeanDefinition registerAspectJAnnotationAutoProxyCreatorIfNecessary(BeanDefinitionRegistry registry, @Nullable Object source) {
return registerOrEscalateApcAsRequired(AnnotationAwareAspectJAutoProxyCreator.class, registry, source);
}

最终注册了 AnnotationAwareAspectJAutoProxyCreator

使用 org.springframework.aop.config.internalAutoProxyCreator 作为名称,获取 AnnotationAwareAspectJAutoProxyCreator 类型的 Bean,并查看其 proxyTargetClass 属性是否为 true

1
2
3
4
5
6
7
8
public static void main(String[] args) {
// --snip--

System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>");
AnnotationAwareAspectJAutoProxyCreator creator =
context.getBean("org.springframework.aop.config.internalAutoProxyCreator", AnnotationAwareAspectJAutoProxyCreator.class);
System.out.println(creator.isProxyTargetClass()); // true
}

【补充】ImportBeanDefinitionRegistrar 接口

将 Bean 注入到 Spring 的大致流程是:

  • 利用 BeanDefinitionReader 读取配置文件或注解信息,为每一个 Bean 生成一个 BeanDefinition
  • BeanDefinition 注册到 BeanDefinitionRegistry
  • 当需要创建 Bean 对象时,从 BeanDefinitionRegistry 中取出对应的 BeanDefinition,利用这个 BeanDefinition 来创建 Bean
  • 如果创建的 Bean 是单例的,Spring 会将这个 Bean 保存到 SingletonBeanRegistry 中,即三级缓存中的第一级缓存,需要时直接从这里获取,而不是重复创建

也就是说 Spring 是通过 BeanDefinition 去创建 Bean 的,而 BeanDefinition 会被注册到 BeanDefinitionRegistry 中,因此可以拿到 BeanDefinitionRegistry 直接向里面注册 BeanDefinition 达到将 Bean 注入到 Spring 的目标。

ImportBeanDefinitionRegistrar 接口就可以直接拿到 BeanDefinitionRegistry

1
2
3
4
5
6
7
8
public interface ImportBeanDefinitionRegistrar {
default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry, BeanNameGenerator importBeanNameGenerator) {
this.registerBeanDefinitions(importingClassMetadata, registry);
}

default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
}
}

该接口需要搭配 @Import 注解使用。

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
public static void main(String[] args) {
// AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
GenericApplicationContext context = new GenericApplicationContext();
// AnnotationConfigUtils.registerAnnotationConfigProcessors(context.getDefaultListableBeanFactory());

context.registerBean(ConfigurationClassPostProcessor.class);
context.registerBean("config", Config.class);
context.refresh();

Arrays.stream(context.getBeanDefinitionNames()).forEach(System.out::println);
System.out.println(context.getBean(User.class));
}

@Configuration
@Import({MyImportBeanDefinitionRegistrar.class})
static class Config {

}

static class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
// 构建 BeanDefinition
AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition(User.class)
.addPropertyValue("name", "mofan")
.addPropertyValue("age", 20)
.getBeanDefinition();
// 注册构建好的 BeanDefinition
registry.registerBeanDefinition("user", beanDefinition);
}
}

@Setter
@ToString
static class User {
private String name;
private int age;
}
org.springframework.context.annotation.ConfigurationClassPostProcessor
config
user
TestImportBeanDefinitionRegistrar.User(name=mofan, age=20)

注意: 使用时一定要确保 Spring 容器中存在 ConfigurationClassPostProcessor 类型的 Bean。

除此之外,使用 BeanDefinitionRegistryPostProcessor 接口也能拿到 BeanDefinitionRegistry

1
2
3
public interface BeanDefinitionRegistryPostProcessor extends BeanFactoryPostProcessor {
void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry var1) throws BeansException;
}

41.3 数据库相关的自动配置

确保当前模块下已导入:

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.0</version>
</dependency>

<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>

DataSource 自动配置

自行实现导入选择器,并使用 @Import 注解进行导入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Configuration
@Import(MyImportSelector.class)
static class Config {

}

static class MyImportSelector implements DeferredImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[]{
DataSourceAutoConfiguration.class.getName(),
MybatisAutoConfiguration.class.getName(),
DataSourceTransactionManagerAutoConfiguration.class.getName(),
TransactionAutoConfiguration.class.getName()
};
}
}

main() 方法中打印导入的 Bean 信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void main(String[] args) {
GenericApplicationContext context = new GenericApplicationContext();
StandardEnvironment env = new StandardEnvironment();
env.getPropertySources().addLast(new SimpleCommandLinePropertySource(
"--spring.datasource.url=jdbc:mysql://localhost:3306/advanced_spring",
"--spring.datasource.username=root",
"--spring.datasource.password=123456"
));
context.setEnvironment(env);
AnnotationConfigUtils.registerAnnotationConfigProcessors(context.getDefaultListableBeanFactory());
context.registerBean(Config.class);

context.refresh();
for (String name : context.getBeanDefinitionNames()) {
String resourceDescription = context.getBeanDefinition(name).getResourceDescription();
if (resourceDescription != null)
System.out.println(name + " 来源: " + resourceDescription);
}
}

未使用配置文件,而是使用 StandardEnvironment 设置了一些数据库连接信息。

最后只打印有明确来源的 Bean 信息,其中有一条:

dataSource 来源: class path resource [org/springframework/boot/autoconfigure/jdbc/DataSourceConfiguration$Hikari.class]

名叫 dataSource 的 Bean 的来源为什么是 DataSourceConfiguration,而不是 DataSourceAutoConfiguration 呢?

查看 DataSourceAutoConfiguration 的源码,实现与 AopAutoConfiguration 类似,都是通过注解来判断需要导入哪些 Bean,有两个关键的内部类 EmbeddedDatabaseConfigurationPooledDataSourceConfiguration

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
@AutoConfiguration(before = SqlInitializationAutoConfiguration.class)
@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
@ConditionalOnMissingBean(type = "io.r2dbc.spi.ConnectionFactory")
@EnableConfigurationProperties(DataSourceProperties.class)
@Import(DataSourcePoolMetadataProvidersConfiguration.class)
public class DataSourceAutoConfiguration {

@Configuration(proxyBeanMethods = false)
@Conditional(EmbeddedDatabaseCondition.class)
@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
@Import(EmbeddedDataSourceConfiguration.class)
protected static class EmbeddedDatabaseConfiguration {

}

@Configuration(proxyBeanMethods = false)
@Conditional(PooledDataSourceCondition.class)
@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
@Import({ DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class,
DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.OracleUcp.class,
DataSourceConfiguration.Generic.class, DataSourceJmxConfiguration.class })
protected static class PooledDataSourceConfiguration {

}
}

它们都被 @Conditional 注解标记。当项目支持内嵌数据源时,EmbeddedDatabaseConfiguration 生效;当项目支持基于数据库连接池的数据源时,PooledDataSourceConfiguration 生效。

SpringBoot 默认的数据库连接池是 Hikari,因此 PooledDataSourceConfiguration 生效,最终使用 @Import 导入一系列 Bean,导入的这些 Bean 都是 DataSourceConfiguration 的内部类,因此dataSource 的 Bean 的来源是 DataSourceConfiguration

DataSourceConfiguration 中,通过 @ConditionalOnClass 注解判断某些 Class 是否存在来使某种数据库连接池生效。

由于导入了 mybatis-spring-boot-starter,其内部依赖 mybatis-spring-boot-jdbc,而它又依赖了 HikariCP,因此最终数据库连接池 Hikari 生效:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(HikariDataSource.class)
@ConditionalOnMissingBean(DataSource.class)
@ConditionalOnProperty(name = "spring.datasource.type", havingValue = "com.zaxxer.hikari.HikariDataSource",
matchIfMissing = true)
static class Hikari {

@Bean
@ConfigurationProperties(prefix = "spring.datasource.hikari")
HikariDataSource dataSource(DataSourceProperties properties) {
HikariDataSource dataSource = createDataSource(properties, HikariDataSource.class);
if (StringUtils.hasText(properties.getName())) {
dataSource.setPoolName(properties.getName());
}
return dataSource;
}

}

Hikari#dataSource() 方法中,接受一个 DataSourceProperties 类型的参数,这要求 Spring 容器中存在 DataSourceProperties 类型的 Bean。

在最初的 DataSourceAutoConfiguration 自动配置类上有个 @EnableConfigurationProperties 注解,它将 DataSourceProperties 添加到容器中:

1
2
3
4
@EnableConfigurationProperties(DataSourceProperties.class)
public class DataSourceAutoConfiguration {
// --snip-=
}

DataSourceProperties 中会绑定配置文件中以 spring.datasource 为前缀的配置:

1
2
3
4
@ConfigurationProperties(prefix = "spring.datasource")
public class DataSourceProperties implements BeanClassLoaderAware, InitializingBean {
// --snip--
}

获取 DataSourceProperties 类型的 Bean,并打印其 urlusernamepassword

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) {
GenericApplicationContext context = new GenericApplicationContext();
StandardEnvironment env = new StandardEnvironment();
env.getPropertySources().addLast(new SimpleCommandLinePropertySource(
"--spring.datasource.url=jdbc:mysql://localhost:3306/advanced_spring",
"--spring.datasource.username=root",
"--spring.datasource.password=123456"
));
context.setEnvironment(env);

// --snip--

DataSourceProperties properties = context.getBean(DataSourceProperties.class);
System.out.println(properties.getUrl());
System.out.println(properties.getUsername());
System.out.println(properties.getPassword());
}
jdbc:mysql://localhost:3306/advanced_spring
root
123456

MyBatis 自动配置

接下来看看 MyBatis 的自动配置类:

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
@Configuration
@ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class})
@ConditionalOnSingleCandidate(DataSource.class)
@EnableConfigurationProperties({MybatisProperties.class})
@AutoConfigureAfter({DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class})
public class MybatisAutoConfiguration implements InitializingBean {
// --snip--

@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
// --snip--
}

// --snip--

@Bean
@ConditionalOnMissingBean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
// --snip--
}

@Configuration
@Import({MybatisAutoConfiguration.AutoConfiguredMapperScannerRegistrar.class})
@ConditionalOnMissingBean({MapperFactoryBean.class, MapperScannerConfigurer.class})
public static class MapperScannerRegistrarNotFoundConfiguration implements InitializingBean {
// --snip--
}

// --snip--
}

MybatisAutoConfiguration 生效的条件有两个:

  • 类路径下存在 SqlSessionFactorySqlSessionFactoryBean
  • Spring 容器中有且仅有一个 DataSource 类型的 Bean

它还添加了 MybatisProperties 类型的 Bean 到 Spring 容器中,并与配置文件中以 mybatis 为前缀的信息绑定。

@AutoConfigureAfter 注解指定了当前自动配置类在 DataSourceAutoConfigurationMybatisLanguageDriverAutoConfiguration 两个自动配置类解析完成之后再解析。

接下来遇到 sqlSessionFactory() 方法:

1
2
3
4
5
@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
// --snip--
}

依赖 Spring 容器中的 DataSource,当容器中不存在 SqlSessionFactory 时,将其添加到 Spring 容器中。

然后是 sqlSessionTemplate() 方法,它与添加 SqlSessionFactory 到 Spring 容器的逻辑一样:

1
2
3
4
5
@Bean
@ConditionalOnMissingBean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
// --snip--
}

SqlSessionTemplate 也是 SqlSession 的实现,提供了与当前线程绑定的 SqlSession。针对多个方法调用,如果它们来自同一个线程,那么获取到的 SqlSession 对象是同一个。这也是为什么有了 DefaultSqlSession 作为 SqlSession 的实现了,还需要 SqlSessionTemplate

在 MyBatis 中,使用 MapperFactoryBean 将接口转换为对象,其核心是 getObject() 方法:

1
2
3
public T getObject() throws Exception {
return this.getSqlSession().getMapper(this.mapperInterface);
}

方法中获取了 sqlSession 对象,而获取的就是 SqlSessionTemplate 对象:

1
2
3
public SqlSession getSqlSession() {
return this.sqlSessionTemplate;
}

最后来到 MapperScannerRegistrarNotFoundConfiguration 内部类:

1
2
3
4
5
6
@Configuration
@Import({MybatisAutoConfiguration.AutoConfiguredMapperScannerRegistrar.class})
@ConditionalOnMissingBean({MapperFactoryBean.class, MapperScannerConfigurer.class})
public static class MapperScannerRegistrarNotFoundConfiguration implements InitializingBean {
// --snip--
}

利用 @ConditionalOnMissingBean 判断 Spring 容器中缺失 MapperFactoryBeanMapperScannerConfigurer 时,该配置类生效。生效时利用 @Import 导入 AutoConfiguredMapperScannerRegistrar

1
2
3
public static class AutoConfiguredMapperScannerRegistrar implements BeanFactoryAware, EnvironmentAware, ImportBeanDefinitionRegistrar {
// --snip--
}

AutoConfiguredMapperScannerRegistrar 实现了 ImportBeanDefinitionRegistrar 接口,允许通过编程的方式加 Bean 添加到 Spring 容器中,而这里是去扫描 Mapper 接口,将其转换为对象添加到 Spring 容器中。

main() 所在类的包路径下创建 mapper 包,并新建三个接口,其中两个被 @Mapper 注解标记:

1
2
3
4
5
6
7
8
9
10
@Mapper
public interface Mapper1 {
}

@Mapper
public interface Mapper2 {
}

public interface Mapper3 {
}

运行 main() 方法,查看 Mapper1Mapper2 是否被添加到 Spring 容器中。

结果是否定的。因为 没有设置要扫描的包路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) {
// --snip--

String packageName = TestDataSourceAuto.class.getPackage().getName();
System.out.println("当前包名: " + packageName);
AutoConfigurationPackages.register(context.getDefaultListableBeanFactory(),
packageName);

context.refresh();
for (String name : context.getBeanDefinitionNames()) {
String resourceDescription = context.getBeanDefinition(name).getResourceDescription();
if (resourceDescription != null)
System.out.println(name + " 来源: " + resourceDescription);
}

// --snip--
}
当前包名: indi.mofan.a41
mapper1 来源: file [D:\Code\IdeaCode\advanced-spring\boot\target\classes\indi\mofan\a41\mapper\Mapper1.class]
mapper2 来源: file [D:\Code\IdeaCode\advanced-spring\boot\target\classes\indi\mofan\a41\mapper\Mapper2.class]

@MapperScan 注解与 MybatisAutoConfiguration 在功能上很类似,只不过:

  • @MapperScan 可以指定具体的扫描路径,未指定时会把引导类范围内的所有接口当做 Mapper 接口;
  • MybatisAutoConfiguration 关注所有被 @Mapper 注解标记的接口,忽略未被 @Mapper 标记的接口。

事务自动配置

事务自动配置与 DataSourceTransactionManagerAutoConfigurationTransactionAutoConfiguration 有关。

DataSourceTransactionManagerAutoConfiguration 配置了 DataSourceTransactionManager 用来执行事务的提交、回滚操作。

TransactionAutoConfiguration 在功能上对标 @EnableTransactionManagement,包含以下三个 Bean:

  • BeanFactoryTransactionAttributeSourceAdvisor:事务切面类,包含通知和切点
  • TransactionInterceptor:事务通知类,由它在目标方法调用前后加入事务操作
  • AnnotationTransactionAttributeSource:解析 @Transactional 及事务属性,还包含了切点功能

如果自定义了 DataSourceTransactionManager 或是在引导类加了 @EnableTransactionManagement,则以自定义为准。

41.4 MVC 自动配置

MVC 的自动配置需要用到四个类:

  • 配置内嵌 Tomcat 服务器工厂:ServletWebServerFactoryAutoConfiguration
  • 配置 DispatcherServlet:DispatcherServletAutoConfiguration
  • 配置 WebMVC 各种组件:WebMvcAutoConfiguration
  • 配置 MVC 的错误处理:ErrorMvcAutoConfiguration

查看自动配置与 MVC 相关的 Bean 的信息、来源:

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
public class TestMvcAuto {
public static void main(String[] args) {
AnnotationConfigServletWebServerApplicationContext context = new AnnotationConfigServletWebServerApplicationContext();
context.registerBean(Config.class);
context.refresh();
for (String name : context.getBeanDefinitionNames()) {
String source = context.getBeanDefinition(name).getResourceDescription();
if (source != null) {
System.out.println(name + " 来源:" + source);
}
}
context.close();
}

@Configuration
@Import(MyImportSelector.class)
static class Config {

}

static class MyImportSelector implements DeferredImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[]{
// 配置内嵌 Tomcat 服务器工厂
ServletWebServerFactoryAutoConfiguration.class.getName(),
// 配置 DispatcherServlet
DispatcherServletAutoConfiguration.class.getName(),
// 配置 WebMVC 各种组件
WebMvcAutoConfiguration.class.getName(),
// 配置 MVC 的错误处理
ErrorMvcAutoConfiguration.class.getName()
};
}
}
}

41.5 自定义自动配置类

在 SpringBoot 自动装配时添加自定义组件分为两步:

  1. 在类路径下自定义 META-INF/spring.factories 文件,以 org.springframework.boot.autoconfigure.EnableAutoConfiguration 为 key,设置需要自动装配的自定义组件的全限定类名为 value
  2. 编写配置类,在配置类上使用 @EnableAutoConfiguration 注解,并将其添加到 Spring 容器中

在实际项目开发中,省略第二步,SpringBoot 的会自动扫描。

SpringBoot 2.7.0 及其以后版本

在类路径下自定义 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件,文件中 每一行 表示需要进行自动装配的类的全限定类名,因此不能随意换行。

在这个文件中,以 # 开头的行表示注释。

42. 条件装配底层

42.1 @Conditional

在 SpringBoot 的自动配置中,经常看到 @Conditional 注解的使用,使用该注解可以按条件加载配置类。

@Conditional 注解并不具备条件判断功能,而是通过指定的 Class 列表来进行判断,指定的 Class 需要实现 Condition 接口。

假设有这样一个需求:通过判断类路径下是否存在 com.alibaba.druid.pool.DruidDataSource 类来加载不同的配置类,当存在 DruidDataSource 时,加载 AutoConfiguration1,反之加载 AutoConfiguration2

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
public static void main(String[] args) throws IOException {
GenericApplicationContext context = new GenericApplicationContext();
context.registerBean("config", Config.class);
context.registerBean(ConfigurationClassPostProcessor.class);
context.refresh();

for (String name : context.getBeanDefinitionNames()) {
System.out.println(name);
}
}

@Configuration
@Import(MyImportSelector.class)
static class Config {
}

static class MyImportSelector implements DeferredImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[]{AutoConfiguration1.class.getName(), AutoConfiguration2.class.getName()};
}
}

static class MyCondition1 implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
// 存在 Druid 依赖
return ClassUtils.isPresent("com.alibaba.druid.pool.DruidDataSource", null);
}
}

static class MyCondition2 implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
// 不存在 Druid 依赖
return !ClassUtils.isPresent("com.alibaba.druid.pool.DruidDataSource", null);
}
}

/**
* 模拟第三方的配置类
*/
@Configuration
@Conditional(MyCondition1.class)
static class AutoConfiguration1 {
@Bean
public Bean1 bean1() {
return new Bean1();
}
}

/**
* 模拟第三方的配置类
*/
@Configuration
@Conditional(MyCondition2.class)
static class AutoConfiguration2 {
@Bean
public Bean2 bean2() {
return new Bean2();
}
}

static class Bean1 {
}

static class Bean2 {
}

此时并未导入 druid 依赖,AutoConfiguration2 应该生效,运行 main() 方法后,控制台打印出:

config
org.springframework.context.annotation.ConfigurationClassPostProcessor
indi.mofan.a42.A42$AutoConfiguration1
bean1

导入 druid 依赖:

1
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.15</version>
</dependency>

再次运行 main() 方法:

config
org.springframework.context.annotation.ConfigurationClassPostProcessor
indi.mofan.a42.A42$AutoConfiguration1
bean1

42.2 @ConditionalOnXxx

在 SpringBoot 的自动配置中,经常看到 @ConditionalOnXxx 注解的使用,这种注解是将某个 @Conditional 的判断进行了封装,比如 ConditionalOnClass 就是用于判断某个 Class 是否存在。

因此针对上文中的代码可以做出修改:

  • 自定义 @ConditionalOnClass 注解,填入需要判断的全限定类名和判断条件;
  • 移除模拟的第三方配置上的 @Conditional 注解,而是使用自定义的 @ConditionalOnClass
  • Condition 接口的使用类重写的 matches() 方法利用 @ConditionalOnClass 注解进行条件判断。
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
public static void main(String[] args) throws IOException {
GenericApplicationContext context = new GenericApplicationContext();
context.registerBean("config", Config.class);
context.registerBean(ConfigurationClassPostProcessor.class);
context.refresh();

for (String name : context.getBeanDefinitionNames()) {
System.out.println(name);
}
}

@Configuration
@Import(MyImportSelector.class)
static class Config {
}

static class MyImportSelector implements DeferredImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[]{AutoConfiguration1.class.getName(), AutoConfiguration2.class.getName()};
}
}

static class MyCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
Map<String, Object> attributes = metadata.getAnnotationAttributes(ConditionalOnClass.class.getName());
Optional<Map<String, Object>> optional = Optional.ofNullable(attributes);
String className = optional.map(i -> String.valueOf(i.get("className"))).orElse("");
boolean exists = optional.map(i -> i.get("exists"))
.map(String::valueOf)
.map(Boolean::parseBoolean).orElse(false);
boolean present = ClassUtils.isPresent(className, null);
return exists == present;
}
}

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
@Conditional(MyCondition.class)
private @interface ConditionalOnClass {
/**
* true 判断存在 false 判断不存在
*/
boolean exists();

/**
* 要判断的类名
*/
String className();
}

@Configuration
@ConditionalOnClass(className = "com.alibaba.druid.pool.DruidDataSource", exists = true)
static class AutoConfiguration1 {
@Bean
public Bean1 bean1() {
return new Bean1();
}
}

@Configuration
@ConditionalOnClass(className = "com.alibaba.druid.pool.DruidDataSource", exists = false)
static class AutoConfiguration2 {
@Bean
public Bean2 bean2() {
return new Bean2();
}
}

static class Bean1 {
}

static class Bean2 {
}

在导入 druid 依赖或未导入 druid 依赖的情况下运行 main() 方法,控制台打印结果与【42.1 @Conditional】一样。