Spring Boot 自动装配原理 / 2
ShawJie

Spring Boot 自动装配原理 / 2

在上一篇内容中,我们简述了关于Spring Boot应用的几个核心注解,核心注解的元注解,以及Spring Boot应用的粗粒度启动过程,还没有看的同学可以去看一看,链接在这里。这一节我们会对自动装配属性的解析,到自动装配的过程进行一个详细的分析论述,本篇源码会比较多,也希望大家耐心阅读。

主类是如何被加载注册的

​ 通过第一篇内容我们可以知道,自动装配注解@EnableAutoConfiguration@SpringBootApplication的元注解,而@SpringBootApplication又注解于我们的应用启动类上,那么我们的应用启动类(即主类)是如何被加载进Spring的容器中的呢?

​ Spring Boot应用通过SpringApplication.run(primarySource, args)方法进行启动,而参数primarySource需要传入的是应用加载的主要来源,即我们主类的类定义。从Spring initializer构建的Demo我们可以看到,primarySource的传入值是DemoApplication.class

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

​ 在Spring Boot应用初始化阶段(上篇有提到,没看过的去看一下,明确一下概念),run方法会构建一个SpirngApplication对象实例,并将我们传入的主类定义保存在对象实例的primarySources字段中。

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

​ 这个字段有什么用我们过会再说,现在先让我们记下SpringApplication对象实例的primarySources储存了我们的启动类定义。

加载的META-INF/spring.factories

​ 我们先顺着往下看deduceFromClasspath()方法,该方法用于推断我们的Spring应用类型(Servlet、Reactive、None),而推断的方法则是通过类加载器对几种Spring应用类型的核心类进行加载,返回true则表示加载成功,反之抛出异常则表示加载失败,类路径中不存在该类。这种推断方式很聪明,在之后的自动装配部分也会有类似的逻辑。再之后是setInitializers()方法,这个方法会设置ApplicationContextInitializerSpringApplication实例,这不是我们的重点,我们的重点在于它的嵌套方法getSpringFactoriesInstances(type),也是我在上篇内容标记的第一个重点。

​ ``getSpringFactoriesInstances()`的内部逻辑如下,具体的操作我用注释标在了代码上面。

1
2
3
4
5
6
7
8
9
10
11
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
// 获取类加载器
ClassLoader classLoader = getClassLoader();
// 从META-INF/spring.factories获取type对应类的全限定名以确保唯一性
Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
// 通过反射构建对象实例
List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
// 根据注解@Order对实例进行排序
AnnotationAwareOrderComparator.sort(instances);
return instances;
}

​ 进入SpringFactoriesLoader.loadFactoryNames(type, classLoader)方法我们就可以看到在之前反复提到的META-INF/spring.factories文件加载过程。在阅读加载过程之前,我们先找一个spring.factories文件看看文件结构。在这边我们就看一下spring-boot-autoconfigure-2.2.5.RELEASE.jar里的META-INF/spring.factories文件内容,spring.factories文件在本质上还是一个properties类型文件,由于该文件内容较多,我将从其中抽取部分展示,具体内容还请各位同学把Jar包Down下来研究一下。

1
2
3
4
5
6
7
8
9
10
11
12
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,\
org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener

org.springframework.context.ApplicationListener=\
org.springframework.boot.autoconfigure.BackgroundPreinitializer

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration,\
...

​ 我从其中抽取了些比较具有代表性的自动装配类,就包括在初始化阶段要加载得初始化器类、应用监听器、以及和我们主题密切相关得自动装配类列表。在自动装配类列表中我抽取了Redis、AOP、MongoDB的自动装配类在此作展示,其实autoconfigure包中还有很多我们日常开发中使用到的许多类库、中间件的自动装配类,譬如ElasticSearch、oauth2、Jap等等,就算autoconfigure包中没有包含我们需要的自动装配类,我们也可以依据此规则自行实现,具体实现方法我会在后面的内容进行分析,此处先行搁置。

​ 在看过spring.factories的文件结构之后,去分析SpringFactoriesLoader.loadFactoryNames方法就会简单很多。传入参数type为需要在spring.factories文件集中检索的key,而返回的结果则是文件集中与之相关的所有类的全限定名。具体逻辑如下。

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
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
// 获取检索类的全限定名
String factoryTypeName = factoryType.getName();
// 进行spring.factories文件集的加载、检索操作,并构以空集为兜底返回数据
return loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());
}

private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
// spring.factories文件集在加载过一次之后会被存入缓存,避免多次进行I/O操作
MultiValueMap<String, String> result = cache.get(classLoader);
if (result != null) {
return result;
}

try {
// 通过类加载器进行类路径下的所有META-INF/spring.factories文件进行扫描
Enumeration<URL> urls = (classLoader != null ?
classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
result = new LinkedMultiValueMap<>();
// 遍历spring.factories文件集
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
UrlResource resource = new UrlResource(url);
// 将spring.factories资源转换为Propperties对象
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());
}
}
}
// 将文件集所有结果以类加载器为Key存入内存
cache.put(classLoader, result);
return result;
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" +
FACTORIES_RESOURCE_LOCATION + "]", ex);
}
}

​ 看完这spring.factories文件的加载逻辑之后,我们已经知晓了自动装配的装配类来源是哪,接下来我们就需要继续弄明白,主类是如何被加载注册进容器,以及spring是如何解析注解在主类上的注解@SpringBootApplication及其元注解EnableAutoConfiguration的。

Spring应用上下文的创建和准备

​ 在完成Spring应用的初始化阶段后,就需要开始进行启动阶段的准备工作,构建Spring应用上下文可以作为这一阶段的核心工作进行解读。

​ 在Spring应用的run方法中我们可以看到Spring应用上下文的构建过程。在Spring应用初始化阶段通过类加载器推断出来的应用类型在此处就派上了用场。Spring会根据不同的应用类型初始化对应的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
protected ConfigurableApplicationContext createApplicationContext() {
Class<?> contextClass = this.applicationContextClass;
if (contextClass == null) {
try {
// 根据不同类型初始化对应的Spring上下文
switch (this.webApplicationType) {
case SERVLET:
contextClass = Class.forName(DEFAULT_SERVLET_WEB_CONTEXT_CLASS);
break;
case REACTIVE:
contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS);
break;
default:
contextClass = Class.forName(DEFAULT_CONTEXT_CLASS);
}
}
catch (ClassNotFoundException ex) {
throw new IllegalStateException(
"Unable create a default ApplicationContext, please specify an ApplicationContextClass", ex);
}
}
// 对类型进行实例化操作
return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass);
}

​ 在初始化Spring上下文的同时,我们的Bean容器,BeanFactory也会同时完成初始化操作。在完成初始化操作之后Spring应用上下文及Bean容器还需要完成一些准备工作以进入应用启动阶段。而主类则是在这一时间点进行的注册装配工作。

​ 在此时间点,Spring上下文会完成环境变量配置、应用初始化器、通知监听器等一系列事件,但与我们所关心(主类装配)的关键代码是这么几行。

1
2
3
Set<Object> sources = getAllSources();
Assert.notEmpty(sources, "Sources must not be empty");
load(context, sources.toArray(new Object[0]));

​ 先通过getAllSources方法获取到Spring应用在初始化及准备阶段所有配置的资源内容。还记得前边说SpringApplication对象实例的primarySources储存了我们的启动类么?在这儿就用上了。我们看一看getAllSources()方法的内部逻辑就明白了。

1
2
3
4
5
6
7
8
9
10
11
12
public Set<Object> getAllSources() {
Set<Object> allSources = new LinkedHashSet<>();
// 获取储存了包括我们应用启动类的主要资源
if (!CollectionUtils.isEmpty(this.primarySources)) {
allSources.addAll(this.primarySources);
}
// 获取附加的配置资源
if (!CollectionUtils.isEmpty(this.sources)) {
allSources.addAll(this.sources);
}
return Collections.unmodifiableSet(allSources);
}

​ 获取到了主类的信息后,就可以通过load(contenxt, source)方法将我们的主类装在进bean容器中了。Load()方法的内部逻辑是Spring应用上下文的BeanFactoy和source构建一个BeanDefinitionLoader,之后检测类资源是否持有@Component注解(由于主类持有@SpringBootApplication注解,而该注解的元注解持有@Configuration注解,@Configuration的元注解中持有@Compontent注解,即持有判定通过),判定通过则进行类注册操作,由于这部分逻辑入栈较多,所以就不在此贴代码了,想要深入了解的同学可以自己跟着方法堆栈看一看。

小结

​ 通过上面的内容,我们了解了Spring boot应用的元注解、自动装配的信息来源以及带有自动装配注解的主类是如何被装载进Bean容器的,Spring应用也进入了应用启动阶段。接下来的部分将会具体分析Spring在启动阶段是如何读取主类的自动装配开关以及自动装配过程是如何自动过滤不需要的自动装配类的(在spring.factories中我们看到了很多自动装配类,但实际我们只需要我们需求的部分,而其他的则需要被过滤掉)。