Spring Boot 运行源码剖析

本文是 Spring Boot 运行源码剖析内容,内容夹杂了不少的 Spring Boot 源代码片段。

有兴趣的读者,可以按照章节名称跳跃阅读。本文分为两大章节,分别是:

  • SpringBoot 工程准备
    • 简单入手 SpringBoot 工程,了解 Web 开发。
  • 从启动类剖析
    • 强迫作者,学习分析源代码。

一、SpringBoot 工程准备

首先,花 2 分钟构建一个 SpringBoot 工程。

pom.xml

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
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>cn.live</groupId>
	<artifactId>springBoot</artifactId>
	<version>0.0.1</version>
	<name>springBoot</name>

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.2.6.RELEASE</version>
	</parent>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-actuator</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-logging</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
		<maven.compiler.encoding>UTF-8</maven.compiler.encoding>
		<java.version>13</java.version>
	</properties>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
				<configuration>
					<fork>true</fork>
				</configuration>
			</plugin>
		</plugins>
	</build>
</project>

application.properties

1
2
spring.profiles.active=dev
spring.devtools.add-properties=false

application.properties

1
2
spring.profiles=dev
logging.level.web=debug

MyApplication.java

1
2
3
4
5
6
7
8
9
10
11
12
package cn.live;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class MyApplication {

    public static void main(String[] args) {
        SpringApplication.run(MyApplication.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
.   ____          _            __ _ _
/\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/  ___)| |_)| | | | | || (_| |  ) ) ) )
'  |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot ::        (v2.2.6.RELEASE)

2020-04-06 16:50:28.783  INFO 4205 --- [  restartedMain] cn.live.MyApplication                    : Starting MyApplication on FoamValue-MBP.local with PID 4205 (/Users/chenxinjie/workspace-2020-02/springBoot/target/classes started by chenxinjie in /Users/chenxinjie/workspace-2020-02/springBoot)
2020-04-06 16:50:28.785  INFO 4205 --- [  restartedMain] cn.live.MyApplication                    : The following profiles are active: dev
2020-04-06 16:50:29.768  INFO 4205 --- [  restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2020-04-06 16:50:29.781  INFO 4205 --- [  restartedMain] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2020-04-06 16:50:29.781  INFO 4205 --- [  restartedMain] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.33]
2020-04-06 16:50:29.857  INFO 4205 --- [  restartedMain] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2020-04-06 16:50:29.857 DEBUG 4205 --- [  restartedMain] o.s.web.context.ContextLoader            : Published root WebApplicationContext as ServletContext attribute with name [org.springframework.web.context.WebApplicationContext.ROOT]
2020-04-06 16:50:29.857  INFO 4205 --- [  restartedMain] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 1020 ms
2020-04-06 16:50:29.997 DEBUG 4205 --- [  restartedMain] o.s.b.w.s.ServletContextInitializerBeans : Mapping filters: filterRegistrationBean urls=[/*] order=-2147483647, characterEncodingFilter urls=[/*] order=-2147483648, formContentFilter urls=[/*] order=-9900, requestContextFilter urls=[/*] order=-105
2020-04-06 16:50:29.997 DEBUG 4205 --- [  restartedMain] o.s.b.w.s.ServletContextInitializerBeans : Mapping servlets: dispatcherServlet urls=[/]
2020-04-06 16:50:30.118  INFO 4205 --- [  restartedMain] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
2020-04-06 16:50:30.129 DEBUG 4205 --- [  restartedMain] s.w.s.m.m.a.RequestMappingHandlerAdapter : ControllerAdvice beans: 0 @ModelAttribute, 0 @InitBinder, 1 RequestBodyAdvice, 1 ResponseBodyAdvice
2020-04-06 16:50:30.183 DEBUG 4205 --- [  restartedMain] s.w.s.m.m.a.RequestMappingHandlerMapping : 2 mappings in 'requestMappingHandlerMapping'
2020-04-06 16:50:30.209 DEBUG 4205 --- [  restartedMain] o.s.w.s.handler.SimpleUrlHandlerMapping  : Patterns [/webjars/**, /**] in 'resourceHandlerMapping'
2020-04-06 16:50:30.219 DEBUG 4205 --- [  restartedMain] .m.m.a.ExceptionHandlerExceptionResolver : ControllerAdvice beans: 0 @ExceptionHandler, 1 ResponseBodyAdvice
2020-04-06 16:50:30.315  INFO 4205 --- [  restartedMain] o.s.b.d.a.OptionalLiveReloadServer       : LiveReload server is running on port 35729
2020-04-06 16:50:30.319  INFO 4205 --- [  restartedMain] o.s.b.a.e.web.EndpointLinksResolver      : Exposing 2 endpoint(s) beneath base path '/actuator'
2020-04-06 16:50:30.370  INFO 4205 --- [  restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2020-04-06 16:50:30.374  INFO 4205 --- [  restartedMain] cn.live.MyApplication                    : Started MyApplication in 1.941 seconds (JVM running for 2.381)

浏览器打开效果

在浏览器中输入 http://localhost:8080/actuator 可以展现如下效果:

1
{"_links":{"self":{"href":"http://localhost:8080/actuator","templated":false},"health":{"href":"http://localhost:8080/actuator/health","templated":false},"health-path":{"href":"http://localhost:8080/actuator/health/{*path}","templated":true},"info":{"href":"http://localhost:8080/actuator/info","templated":false}}}

小结

依稀还记得 2011 年那会儿,Spring 还没有现在这么流行。那时更多的还是,手动下载一个个的 jar 包,然后放在一个 Java Web 工程下面去尝试。Spring Framework + Struts2 + Hibernate 的架构,包含很多 jar 的基础上,还有一些特定版本冲突的问题。那时候,从零构建一个 Java Web 工程,是一件非常苦痛的任务。

而现在,Spring Boot + Maven 的方式,简直不要太香了。

二、从启动类剖析

MyApplication.java 是开发人员自定义的启动类,但实际上有效的代码只有两行。分别是 @SpringBootApplicationSpringApplication.run()

@SpringBootApplicationSpringApplication.run() 两个角度来拆分的话,可以得到下面的树状结构。

1
2
3
4
5
6
7
8
9
10
11
12
MyApplication.java
|-- @SpringBootApplication
|   |-- @SpringBootConfiguration
|       |-- @Configuration
|   |-- @EnableAutoConfiguration
|       |-- @AutoConfigurationPackage
|           |-- @Import(AutoConfigurationPackages.Registrar.class)
|       |-- @Import(AutoConfigurationImportSelector.class)
|   |-- @ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
|       |-- @Repeatable(ComponentScans.class)
|           |-- @ComponentScans
|--SpringApplication.run()

SpringBootApplication 注解部分

Java 注解用于为 Java 代码提供元数据。作为元数据,注解不直接影响你的代码执行,但也有一些类型的注解实际上可以用于这一目的。

  1. SpringBootApplication 注解接口类
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
		@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
  • @SpringBootApplication 的作用范围在接口、类、枚举、注解接口类。
  • @SpringBootApplication 在 class 字节码文件中存在,运行时可以通过反射获取到。
  • @SpringBootApplication 将被包含在 javadoc 中去。
  • @SpringBootApplication 修饰的类,它的子类可以继承 @SpringBootApplication 注解。
  • @SpringBootApplication 等效于同时使用 @SpringBootConfiguration、@EnableAutoConfiguration、@ComponentScan 三个注解。
  1. SpringBootConfiguration 注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
  • @SpringBootConfiguration 的作用范围在接口、类、枚举、注解接口类。
  • @SpringBootConfiguration 在 class 字节码文件中存在,运行时可以通过反射获取到。
  • @SpringBootConfiguration 将被包含在 javadoc 中去。
  1. Configuration 注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
  • @Configuration 的作用范围在接口、类、枚举、注解接口类。
  • @Configuration 在 class 字节码文件中存在,运行时可以通过反射获取到。
  • @Configuration 将被包含在 javadoc 中去。
  • @Configuration 注解使用 @Component 之后,会被实例化到 Spring 容器中。
  1. EnableAutoConfiguration 注解
1
2
3
4
5
6
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
  • @EnableAutoConfiguration 的作用范围在接口、类、枚举、注解接口类。
  • @EnableAutoConfiguration 在 class 字节码文件中存在,运行时可以通过反射获取到。
  • @EnableAutoConfiguration 将被包含在 javadoc 中去。
  • @EnableAutoConfiguration 修饰的类,它的子类可以继承 @EnableAutoConfiguration 注解。
  • @EnableAutoConfiguration 修饰的类,会将 AutoConfigurationImportSelector 自动注入到 Spring 容器中。
  1. AutoConfigurationPackage 注解
1
2
3
4
5
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(AutoConfigurationPackages.Registrar.class)
  • @AutoConfigurationPackage 的作用范围在接口、类、枚举、注解接口类。
  • @AutoConfigurationPackage 在 class 字节码文件中存在,运行时可以通过反射获取到。
  • @AutoConfigurationPackage 将被包含在 javadoc 中去。
  • @AutoConfigurationPackage 修饰的类,它的子类可以继承 @AutoConfigurationPackage 注解。
  • @AutoConfigurationPackage 修饰的类,会将 AutoConfigurationPackages.Registrar 自动注入到 Spring 容器中。
  1. ComponentScan 注解
1
2
3
4
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Repeatable(ComponentScans.class)
  • @ComponentScan 在 class 字节码文件中存在,运行时可以通过反射获取到。
  • @ComponentScan 的作用范围在接口、类、枚举、注解接口类。
  • @ComponentScan 将被包含在 javadoc 中去。
  1. ComponentScans 注解
1
2
3
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
  • @ComponentScan 在 class 字节码文件中存在,运行时可以通过反射获取到。
  • @ComponentScan 的作用范围在接口、类、枚举、注解接口类。
  • @ComponentScan 将被包含在 javadoc 中去。

以上是 @SpringBootApplication 注解的展开内容。所有的注解加起来,只做了两件事。

  • 标注
  • 将指定类注入到 Spring 容器中
    • AutoConfigurationImportSelector
    • AutoConfigurationPackages.Registrar
    • TypeExcludeFilter
    • AutoConfigurationExcludeFilter

SpringApplication.run() 部分

  1. new SpringApplication(primarySources)
1
2
3
4
5
6
7
8
9
10
@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();
		setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
		setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
		this.mainApplicationClass = deduceMainApplicationClass();
	}
  • 初始化 resourceLoader
  • 初始化 PrimarySources
  • 初始化 webApplicationType
  • 初始化 ApplicationContext 构造器
  • 初始化 ApplicationListener 应用监听器
  • 初始化 mainApplicationClass
  1. SpringApplication.run(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
public ConfigurableApplicationContext run(String... args) {
	StopWatch stopWatch = new StopWatch();
	stopWatch.start();
	ConfigurableApplicationContext context = null;
	Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
	configureHeadlessProperty();
	SpringApplicationRunListeners listeners = getRunListeners(args);
	listeners.starting();
	try {
		ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
		ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
		configureIgnoreBeanInfo(environment);
		Banner printedBanner = printBanner(environment);
		context = createApplicationContext();
		exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class, new Class[] { ConfigurableApplicationContext.class }, context);
		prepareContext(context, environment, listeners, applicationArguments, printedBanner);
		refreshContext(context);
		afterRefresh(context, applicationArguments);
		stopWatch.stop();
		if (this.logStartupInfo) {
			new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
		}
		listeners.started(context);
		callRunners(context, applicationArguments);
	}
	catch (Throwable ex) {
		handleRunFailure(context, ex, exceptionReporters, listeners);
		throw new IllegalStateException(ex);
	}

	try {
		listeners.running(context);
	}
	catch (Throwable ex) {
		handleRunFailure(context, ex, exceptionReporters, null);
		throw new IllegalStateException(ex);
	}
	return context;
}
  • 初始化应用计数器,并启动。
  • 初始化应用监听组,并启动。
  • 准备环境变量
  • Banner 彩蛋
  • 创建 ApplicationContext
  • 获取排除工厂实例
  • 准备 ApplicationContext
  • 刷新 ApplicationContext
  • 刷新后处理 ApplicationContext
  • 停止应用计数器
  • 回调监听组
  • 回调 ApplicationRunner、CommandLineRunner 接口
  • 运行应用监听组

小结

简单翻阅了源代码,了解 @SpringBootApplication 都包含哪些注解标记。以及 SpringApplication.run() 执行的之后,都有哪些步骤。