0%

SpringBoot 系列(一)从启动开始

引言

SpringBoot 的出现,大大简化了 Spring 依赖配置,让我们可以快速搭起 Web 项目。本系列文章将带你从源码角度分析 SpringBoot 背后的原理。

run() 方法

我们知道,大部分语言都以 main 为起点,SpringBoot 也不例外,一个标准的 SpringBoot 启动类如下:

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

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

该类没有什么特殊的地方,只有一个 @SpringBootApplication 注解,外加一个 SpringApplication.run(DemoApplication.class, args); 方法。要想启动 SpringBoot 项目,只需要运行这个 main 方法即可,而该 main 方法做的唯一一件事就是调用 SpringApplication.run() 方法。所以我们把焦点放到这个方法,稍后我们在讨论 @SpringBootApplication 这个注解。

我们打开这个 run() 方法,可以看到该方法有一系列的重载方法,而我们入口方法签名如下:

1
2
3
4
5
6
7
8
9
    /* Static helper that can be used to run a {@link SpringApplication} from the
* specified source using default settings.
* @param primarySource the primary source to load
* @param args the application arguments (usually passed from a Java main method)
* @return the running {@link ApplicationContext}
*/
public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
return run(new Class<?>[] { primarySource }, args);
}

入参

  • primarySource
    从注解中,我们看到该参数是需要载入信息的主要来源,至于有什么作用,目前还不得而知,这里我们只需要记住,我们把 DemoApplication 这个带有 @SpringBootApplication 注解的类传了进来。

  • args
    这个就是我们传递给 java 的命令行参数。

返回值

  • ConfigurableApplicationContext
    通过后缀 ApplicationContext 我们知道,它是一个 Bean 容器,而且是一个高级容器。具体作用,目前也是未知。

我们顺着这个 run 方法继续向下分析,发现它创建了 SpringApplication 对象:

1
2
3
public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
return new SpringApplication(primarySources).run(args);
}

然后调用该对象的 run 方法。SpringApplication 对象的属性多的吓人,这里我也就不列举了。我们继续往下分析(然后再往回推,看看那些属性的作用是什么),我们最终来到的 run 方法体如下:

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
	/**
* Run the Spring application, creating and refreshing a new
* {@link ApplicationContext}.
* @param args the application arguments (usually passed from a Java main method)
* @return a running {@link ApplicationContext}
*/
public ConfigurableApplicationContext run(String... args) {
// 启动计时器,用于计算启动耗时
StopWatch stopWatch = new StopWatch();
stopWatch.start();
// 这个 context 我们需要注意一下,看接下来它的实例是什么
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 信息
Banner printedBanner = printBanner(environment);
// 这里创建出来的容器实例其实就是 `org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext`
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);
}
// 监听器生命周期 - started
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);
}
// 这里返回来刚刚创建出来的 bean 容器
return context;
}

这个方法就是启动整个 SpringBoot 应用的核心方法了。内容不算太多,我把大体的流程用注释的方式进行说明。然后选择一个主线程,进行讲解。

启动监听器

在容器创建之前,会先进行监听器的获取与启动,我们来分析一下 SpringBoot 是如何拿到以及拿到哪些监听器的。

1
2
3
4
5
private SpringApplicationRunListeners getRunListeners(String[] args) {
Class<?>[] types = new Class<?>[] { SpringApplication.class, String[].class };
return new SpringApplicationRunListeners(logger,
getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args));
}

从以上代码中我们看到,监听器的获取由 getSpringFactoriesInstances() 完成,(注意参数中的 this,也就是当前 SpringApplication 对象,它也就间接的传递给了监听器,用于构造实例)。我们看下它的实现。

1
2
3
4
5
6
7
8
9
10
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
ClassLoader classLoader = getClassLoader();
// 获取监听器类的全限定名,注意这里用了 set,避免实例重复创建
Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
// 创建出具体的监听器实例
List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
// 对监听器实例进行排序
AnnotationAwareOrderComparator.sort(instances);
return instances;
}

参数 type 通过调用我们知道其值为 SpringApplicationRunListenergetSpringFactoriesInstances() 又将载入实现委托给了 SpringFactoriesLoader.loadFactoryNames()。通过接下来调用的函数的名字我们也可以猜到,这一步是获取所有的目标类,然后通过 createSpringFactoriesInstances() 创建出具体的实例,排序后返回。

loadSpringFactories()

这里之所以把该方法单独拿出来是因为大部分的 Starter 组件中的 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
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
MultiValueMap<String, String> result = cache.get(classLoader);
if (result != null) {
return result;
}

try {
Enumeration<URL> urls = (classLoader != null ?
classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
result = new LinkedMultiValueMap<>();
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
for (Map.Entry<?, ?> entry : properties.entrySet()) {
String factoryTypeName = ((String) entry.getKey()).trim();
for (String factoryImplementationName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
result.add(factoryTypeName, factoryImplementationName.trim());
}
}
}
cache.put(classLoader, result);
return result;
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" +
FACTORIES_RESOURCE_LOCATION + "]", ex);
}
}

该方法其实没有什么特别的地方,基本上就是属性的获取之类的。唯一需要关注的就是 FACTORIES_RESOURCE_LOCATION 变量,而该变量的值为 public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories"; 。说到这,我想大家应该明白了,SpringBoot 就是到这个约定的路径下,到 spring.factories 文件中获取我们想要添加到系统容器中的 bean 信息。而 SpringBoot 的 starter 组件也是通过这个方式加载的。

SpringApplicationRunListeners

SpringBoot 获取到的 SpringApplicationRunListener 类型的监听器其实就是在 spring-boot-x.x.x.RELEASE.jar 包下的 EventPublishingRunListener 监听器。然后将获取到的监听器保存到了 SpringApplicationRunListeners 中的 listener 集合中,我们随后看到 SpringBoot 调用了该监听器类的 starting() 方法,随之而来的就是触发 SpringApplicationRunListeners 中保存的所有监听器的 starting() 方法。我们这里只看 EventPublishingRunListener 的 starting() 方法实现。

1
2
3
public void starting() {
this.initialMulticaster.multicastEvent(new ApplicationStartingEvent(this.application, this.args));
}

这里对事件进行了广播,广播对象为 SimpleApplicationEventMulticaster (SpringBoot 对事件的广播都由该对象完成)。事件对象参数中的 this.application 就是之前传进来的 SpringApplication。

准备上下文

启动监听器之后,就开始了上下文的准备阶段,这里就会加载一批 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
37
38
39
40
private void prepareContext(ConfigurableApplicationContext context, ConfigurableEnvironment environment,
SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) {
// 为当前容器配置环境对象
context.setEnvironment(environment);
// 容器后置处理器
postProcessApplicationContext(context);
// 初始化器回调
applyInitializers(context);
// 监听器生命周期 - contextPrepared
listeners.contextPrepared(context);
if (this.logStartupInfo) {
logStartupInfo(context.getParent() == null);
logStartupProfileInfo(context);
}
// 这里获取当前高级容器中的低级容器,这里为 DefaultListableBeanFactory 实例
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
// 注册单例 bean
beanFactory.registerSingleton("springApplicationArguments", applicationArguments);

if (printedBanner != null) {
beanFactory.registerSingleton("springBootBanner", printedBanner);
}
// 这里用来设置是否允许 bean 定义覆盖
if (beanFactory instanceof DefaultListableBeanFactory) {
((DefaultListableBeanFactory) beanFactory)
.setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
}
// 懒加载处理器配置
if (this.lazyInitialization) {
context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor());
}

// 从这里开始,加载 bean,而这里的 source 就包括了我们调用 run 方法时传进来的入口类
Set<Object> sources = getAllSources();
Assert.notEmpty(sources, "Sources must not be empty");
// 开始载入
load(context, sources.toArray(new Object[0]));
// 监听器生命周期 - contextLoaded
listeners.contextLoaded(context);
}

首先为当前容器对象设置环境变量对象,然后开始配置容器的后置处理器:

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
/**
* Apply any relevant post processing the {@link ApplicationContext}. Subclasses can
* apply additional processing as required.
* @param context the application context
*/
protected void postProcessApplicationContext(ConfigurableApplicationContext context) {
// bean 名称生成器
if (this.beanNameGenerator != null) {
context.getBeanFactory().registerSingleton(AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR,
this.beanNameGenerator);
}
// 资源载入器
if (this.resourceLoader != null) {
if (context instanceof GenericApplicationContext) {
((GenericApplicationContext) context).setResourceLoader(this.resourceLoader);
}
if (context instanceof DefaultResourceLoader) {
((DefaultResourceLoader) context).setClassLoader(this.resourceLoader.getClassLoader());
}
}
// 添加转换服务
if (this.addConversionService) {
context.getBeanFactory().setConversionService(ApplicationConversionService.getSharedInstance());
}
}

不熟悉没关系,这里我们只是留个印象,知道有这么个过程,至于它们的具体作用,在后续分析源码时在了解也不晚。再下来就是开始应用各个初始化器以及监听器的回调了。这里就不详细展开了,记住的只是每个阶段的生命周期,好在我们后续需要的时候能够随心所欲。

📚Tips

这里我们把 ApplicationContext 的实例称作高级容器,将 BeanFactory 称作低级容器。

load() 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
	/**
* Load beans into the application context.
* @param context the context to load beans into
* @param sources the sources to load
*/
protected void load(ApplicationContext context, Object[] sources) {
if (logger.isDebugEnabled()) {
logger.debug("Loading source " + StringUtils.arrayToCommaDelimitedString(sources));
}
BeanDefinitionLoader loader = createBeanDefinitionLoader(getBeanDefinitionRegistry(context), sources);
if (this.beanNameGenerator != null) {
loader.setBeanNameGenerator(this.beanNameGenerator);
}
if (this.resourceLoader != null) {
loader.setResourceLoader(this.resourceLoader);
}
if (this.environment != null) {
loader.setEnvironment(this.environment);
}
loader.load();
}

该 load 方法就是将入口类中涉及到的 bean 注册到 bean 容器中。其中 getBeanDefinitionRegistry 方法用于获取 bean 容器对象的 bean 定义注册器,因为 AnnotationConfigServletWebServerApplicationContext 默认实现了注册接口,所以这里返回的其实是容器对象本身。这里我们还需要注意的是 createBeanDefinitionLoader() 方法,我们看下它的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* Create a new {@link BeanDefinitionLoader} that will load beans into the specified
* {@link BeanDefinitionRegistry}.
* @param registry the bean definition registry that will contain the loaded beans
* @param sources the bean sources
*/
BeanDefinitionLoader(BeanDefinitionRegistry registry, Object... sources) {
Assert.notNull(registry, "Registry must not be null");
Assert.notEmpty(sources, "Sources must not be empty");
this.sources = sources;
this.annotatedReader = new AnnotatedBeanDefinitionReader(registry);
this.xmlReader = new XmlBeanDefinitionReader(registry);
if (isGroovyPresent()) {
this.groovyReader = new GroovyBeanDefinitionReader(registry);
}
this.scanner = new ClassPathBeanDefinitionScanner(registry);
this.scanner.addExcludeFilter(new ClassExcludeFilter(sources));
}

从代码中我们看到,这里创建了一个基于注解的读取器 — AnnotatedBeanDefinitionReader 以及一个类路径扫描器 — ClassPathBeanDefinitionScanner。这两个对象的入参都是 bean 容器。也就是说这两个方法会把获取到的 bean 信息存到 bean 容器中。

由于 load 调用路径过深,我这里就直接贴出最终的 load 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
private int load(Class<?> source) {
if (isGroovyPresent() && GroovyBeanDefinitionSource.class.isAssignableFrom(source)) {
// Any GroovyLoaders added in beans{} DSL can contribute beans here
GroovyBeanDefinitionSource loader = BeanUtils.instantiateClass(source, GroovyBeanDefinitionSource.class);
load(loader);
}
// 以下是真正执行的逻辑
if (isComponent(source)) {
this.annotatedReader.register(source);
return 1;
}
return 0;
}

这里我们看到了一个”很熟悉”的方法 isComponent(source),通过它的名字我们可以猜出,它就是用来判断当前源是否包含 @Component 注解。如果是,开始调用基于注解的 bean 读取器注册相关信息。

1
2
3
4
5
6
7
8
9
10
11
12
	/**
* Register one or more component classes to be processed.
* <p>Calls to {@code register} are idempotent; adding the same
* component class more than once has no additional effect.
* @param componentClasses one or more component classes,
* e.g. {@link Configuration @Configuration} classes
*/
public void register(Class<?>... componentClasses) {
for (Class<?> componentClass : componentClasses) {
registerBean(componentClass);
}
}

registerBean() 方法会将注册逻辑委托给 doRegisterBean() 方法。那么 bean 的定义信息最终会注册到什么地方呢?当然是 DefaultListableBeanFactory 中,对于 bean 的别名则是存到了 DefaultListableBeanFactory 的父类 SimpleAliasRegistry 中。

到目前为止,完成了 bean 定义的注册,这也是 prepareContext() 方法做的工作。接下来执行的就是上下文的刷新了,不过我们本文不讨论该方法,而是把它放到下一篇文章中,这么做一方面是因为该方法内容篇幅较大;另一方面是因为我们还有另一个知识点没有说明,那就是 @SpringBootApplication 注解。

@SpringBootApplication 注解

1
2
3
4
5
6
7
8
@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 注解中有一个比较熟悉的元注解 @ComponentScan 当然还有两个新注解 @SpringBootConfiguration@EnableAutoConfiguration。我们看下比较陌生的两个注解。

@SpringBootConfiguration

1
2
3
4
5
6
7
8
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
public @interface SpringBootConfiguration {
@AliasFor(annotation = Configuration.class)
boolean proxyBeanMethods() default true;
}

这个注解比较简单,就是组合了 @Configuration 注解,并且多了一个同 @Configuration 一样的属性 proxyBeanMethods() 该属性就是用来标记当前的 @Configuration 下带有 @Bean 方法的代理规则,以此来提升性能。

@EnableAutoConfiguration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {

String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

/**
* Exclude specific auto-configuration classes such that they will never be applied.
* @return the classes to exclude
*/
Class<?>[] exclude() default {};

/**
* Exclude specific auto-configuration class names such that they will never be
* applied.
* @return the class names to exclude
* @since 1.3.0
*/
String[] excludeName() default {};

该注解其实引入了两个配置类:AutoConfigurationImportSelector 以及 AutoConfigurationPackages.Registrar。那么它们的作用是什么?不知道你有没有好奇过,SpringBoot 是怎么找到 starter 组件中的 bean,并且加载到当前容器中的?其实就是 AutoConfigurationImportSelector 中的逻辑实现的,它会去约定好的路径文件中读取 bean 信息,也就是 META-INF/spring.factories