Spring Boot源码分析-配置文件加载原理

Spring Boot是目前最流行的Java开发框架,它提供了很多的默认配置,不需要我们再去逐一配置,极大地简化了开发流程。项目中的部分具体配置值一般都写在application.propertiesapplication.yml中,本文就让我们一起来探讨一下Spring Boot如何加载配置文件中的内容。

本文针对有一定Spring Boot使用基础的同学,才能更好地理解后面叙述的内容。

基于Spring Boot 1.5.6.RELEASE版本

首先让我们了解一下Spring Boot

  • 简介(以下内容来自百度百科

    Spring Boot是由Pivotal团队提供的全新框架,其设计目的是用来简化新Spring应用的初始搭建以及开发过程。该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置。通过这种方式,Spring Boot致力于在蓬勃发展的快速应用开发领域(rapid application development)成为领导者。

  • Spring Boot特性

  1. 约定优于配置,大多数的配置直接使用默认配置即可
  2. 快速搭建项目,脱离繁杂的XML配置
  3. 内嵌Servlet容器,不依赖外部容器
  4. 与云计算天然集成,提供主流框架一键集成方式

接下来,进入正题通过源码分析理解Spring Boot加载配置文件内容的过程(部分方法内容没有贴出,可以根据源码一步一步跟踪下去)。

所有的Spring Boot项目都是由SpringApplication.run()这个静态方法开始执行的,源码分析也从这里开始进行。

1
2
3
public static ConfigurableApplicationContext run(Object source, String... args) {
return run(new Object[] { source }, args);
}

run()方法内部实际上构造了一个SpringApplication对象并执行对象的run()方法(非静态方法)

1
2
3
public static ConfigurableApplicationContext run(Object[] sources, String[] args) {
return new SpringApplication(sources).run(args);
}

可以看到构造方法里面执行了initialize()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
private void initialize(Object[] sources) {
if (sources != null && sources.length > 0) {
this.sources.addAll(Arrays.asList(sources));
}
// 判断当前是否是web环境
this.webEnvironment = deduceWebEnvironment();
// 初始化ApplicationContextInitializer对象
setInitializers((Collection) getSpringFactoriesInstances(
ApplicationContextInitializer.class));
// 初始化ApplicationListener对象
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
this.mainApplicationClass = deduceMainApplicationClass();
}

这里可以看到是通过getSpringFactoriesInstances()初始化相关对象,进入方法内部

1
2
3
4
5
6
7
8
9
10
11
12
private <T> Collection<? extends T> getSpringFactoriesInstances(Class<T> type,
Class<?>[] parameterTypes, Object... args) {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
// 加载类名称
Set<String> names = new LinkedHashSet<String>(
SpringFactoriesLoader.loadFactoryNames(type, classLoader));
// 创建对象
List<T> instances = createSpringFactoriesInstances(type, parameterTypes,
classLoader, args, names);
AnnotationAwareOrderComparator.sort(instances);
return instances;
}

进入SpringFactoriesLoader.loadFactoryNames()方法,看加载哪些类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";

public static List<String> loadFactoryNames(Class<?> factoryClass, ClassLoader classLoader) {
String factoryClassName = factoryClass.getName();
try {
Enumeration<URL> urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
List<String> result = new ArrayList<String>();
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url));
String factoryClassNames = properties.getProperty(factoryClassName);
result.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(factoryClassNames)));
}
return result;
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load [" + factoryClass.getName() +
"] factories from location [" + FACTORIES_RESOURCE_LOCATION + "]", ex);
}
}

可以看到实际上是加载META-INF/spring.factories文件夹下的内容,读取到的内容放在Properties中,通过类的名称去获取对应的值。值就是实现类的名称,然后再调用createSpringFactoriesInstances创建相关类的实例,这样就完成了对ApplicationContextInitializer对象的实例化工作。同理ApplicationListener

那么META-INF/spring.factories这个文件放在哪里呢,我们的项目下一般是不存在这个文件的。

实际上这个文件存在于Spring Boot项目中,这里就体现了Spring Boot的默认配置,文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# PropertySource Loaders
org.springframework.boot.env.PropertySourceLoader=\
org.springframework.boot.env.PropertiesPropertySourceLoader,\
org.springframework.boot.env.YamlPropertySourceLoader

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

# Application Context Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.context.ConfigurationWarningsApplicationContextInitializer,\
org.springframework.boot.context.ContextIdApplicationContextInitializer,\
org.springframework.boot.context.config.DelegatingApplicationContextInitializer,\
org.springframework.boot.context.embedded.ServerPortInfoApplicationContextInitializer

# Application Listeners
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.ConfigFileApplicationListener,\
org.springframework.boot.context.config.DelegatingApplicationListener,\
org.springframework.boot.liquibase.LiquibaseServiceLocatorApplicationListener,\
org.springframework.boot.logging.ClasspathLoggingApplicationListener,\
org.springframework.boot.logging.LoggingApplicationListener

# Environment Post Processors
org.springframework.boot.env.EnvironmentPostProcessor=\
org.springframework.boot.cloud.CloudFoundryVcapEnvironmentPostProcessor,\
org.springframework.boot.env.SpringApplicationJsonEnvironmentPostProcessor

# Failure Analyzers
org.springframework.boot.diagnostics.FailureAnalyzer=\
org.springframework.boot.diagnostics.analyzer.BeanCurrentlyInCreationFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.BeanNotOfRequiredTypeFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.BindFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.ConnectorStartFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.NoSuchMethodFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.NoUniqueBeanDefinitionFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.PortInUseFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.ValidationExceptionFailureAnalyzer

# FailureAnalysisReporters
org.springframework.boot.diagnostics.FailureAnalysisReporter=\
org.springframework.boot.diagnostics.LoggingFailureAnalysisReporter

接下来,继续看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
public ConfigurableApplicationContext run(String... args) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ConfigurableApplicationContext context = null;
FailureAnalyzers analyzers = null;
configureHeadlessProperty();
// 实例化SpringApplicationRunListener对象
SpringApplicationRunListeners listeners = getRunListeners(args);
// 广播ApplicationStartedEvent事件
listeners.starting();
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(
args);
ConfigurableEnvironment environment = prepareEnvironment(listeners,
applicationArguments);
Banner printedBanner = printBanner(environment);
context = createApplicationContext();
analyzers = new FailureAnalyzers(context);
prepareContext(context, environment, listeners, applicationArguments,
printedBanner);
refreshContext(context);
afterRefresh(context, applicationArguments);
listeners.finished(context, null);
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass)
.logStarted(getApplicationLog(), stopWatch);
}
return context;
}
catch (Throwable ex) {
handleRunFailure(context, listeners, analyzers, ex);
throw new IllegalStateException(ex);
}
}

这里需要关注getRunListeners()这个方法

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));
}

根据前面分析的代码可以看出,这里构造了SpringApplicationRunListener对象,对应的实现类即是EventPublishingRunListener,这个对象提供了广播Spring事件的能力。

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

再看listeners.starting(),实际上就是执行了EventPublishingRunListener.starting()方法,这个方法广播了一个ApplicationEnvironmentPreparedEvent事件。


到这里才真正开始进入读取配置的环节,前面的都是铺垫。
通过观察可以看出ConfigFileApplicationListener监听了ApplicationEnvironmentPreparedEvent事件。那么在接收到这个事件的通知之后,又做了什么呢?

继续往下看…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof ApplicationEnvironmentPreparedEvent) {
onApplicationEnvironmentPreparedEvent(
(ApplicationEnvironmentPreparedEvent) event);
}
if (event instanceof ApplicationPreparedEvent) {
onApplicationPreparedEvent(event);
}
}

private void onApplicationEnvironmentPreparedEvent(
ApplicationEnvironmentPreparedEvent event) {
List<EnvironmentPostProcessor> postProcessors = loadPostProcessors();
postProcessors.add(this);
AnnotationAwareOrderComparator.sort(postProcessors);
for (EnvironmentPostProcessor postProcessor : postProcessors) {
postProcessor.postProcessEnvironment(event.getEnvironment(),
event.getSpringApplication());
}
}

这里可以看到执行了onApplicationEnvironmentPreparedEvent()方法。首先加载通过SpringFactoriesLoader.loadFactories()加载对应的Processor,然后将ConfigFileApplicationListener也添加到了postProcessors中。原来它也实现了EnvironmentPostProcessor接口。

1
2
3
public class ConfigFileApplicationListener
implements EnvironmentPostProcessor, SmartApplicationListener, Ordered {
}

紧接着执行每个ProcessorpostProcessEnvironment()方法。这里我们主要关注ConfigFileApplicationListener.postProcessEnvironment()方法。

1
2
3
4
5
6
7
8
9
10
11
12
public void postProcessEnvironment(ConfigurableEnvironment environment,
SpringApplication application) {
addPropertySources(environment, application.getResourceLoader());
configureIgnoreBeanInfo(environment);
bindToSpringApplication(environment, application);
}
protected void addPropertySources(ConfigurableEnvironment environment,
ResourceLoader resourceLoader) {
RandomValuePropertySource.addToEnvironment(environment);
// load方法才真正实现加载配置文件内容
new Loader(environment, resourceLoader).load();
}

继续跟踪Loader.load()方法

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 void load() {
this.propertiesLoader = new PropertySourcesLoader();
this.activatedProfiles = false;
this.profiles = Collections.asLifoQueue(new LinkedList<Profile>());
this.processedProfiles = new LinkedList<Profile>();

Set<Profile> initialActiveProfiles = initializeActiveProfiles();
this.profiles.addAll(getUnprocessedActiveProfiles(initialActiveProfiles));
if (this.profiles.isEmpty()) {
for (String defaultProfileName : this.environment.getDefaultProfiles()) {
Profile defaultProfile = new Profile(defaultProfileName, true);
if (!this.profiles.contains(defaultProfile)) {
this.profiles.add(defaultProfile);
}
}
}

this.profiles.add(null);

while (!this.profiles.isEmpty()) {
Profile profile = this.profiles.poll();
for (String location : getSearchLocations()) {
if (!location.endsWith("/")) {
load(location, null, profile);
}
else {
for (String name : getSearchNames()) {
load(location, name, profile);
}
}
}
this.processedProfiles.add(profile);
}

addConfigurationProperties(this.propertiesLoader.getPropertySources());
}

这里先不考虑profile的影响,直接看while循环里面且套的foreach循环,进入getSearchLocations()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static final String CONFIG_LOCATION_PROPERTY = "spring.config.location";
private static final String DEFAULT_SEARCH_LOCATIONS = "classpath:/,classpath:/config/,file:./,file:./config/";
private Set<String> getSearchLocations() {
Set<String> locations = new LinkedHashSet<String>();
// 判断是否修改了配置文件的默认位置
if (this.environment.containsProperty(CONFIG_LOCATION_PROPERTY)) {
for (String path : asResolvedSet(
this.environment.getProperty(CONFIG_LOCATION_PROPERTY), null)) {
if (!path.contains("$")) {
path = StringUtils.cleanPath(path);
if (!ResourceUtils.isUrl(path)) {
path = ResourceUtils.FILE_URL_PREFIX + path;
}
}
locations.add(path);
}
}
locations.addAll(
asResolvedSet(ConfigFileApplicationListener.this.searchLocations,
DEFAULT_SEARCH_LOCATIONS));
return locations;
}

DEFAULT_SEARCH_LOCATIONS可以看出,通常情况下,这个四个位置的配置文件会被默认加载。在回到load()方法里面的foreach循环,会执行

1
2
3
for (String name : getSearchNames()) {
load(location, name, profile);
}

在进入getSearchNames()方法

1
2
3
4
5
6
7
8
private static final String DEFAULT_NAMES = "application";
private Set<String> getSearchNames() {
if (this.environment.containsProperty(CONFIG_NAME_PROPERTY)) {
return asResolvedSet(this.environment.getProperty(CONFIG_NAME_PROPERTY),
null);
}
return asResolvedSet(ConfigFileApplicationListener.this.names, DEFAULT_NAMES);
}

这里的ConfigFileApplicationListener.this.namesnull,所以返回的就是DEFAULT_NAMES。所以,这就是为什么默认配置文件名称都是application
拿到文件名称后,在回到上面的forech方法,执行了load()方法,name即是文件名称

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
private void load(String location, String name, Profile profile) {
String group = "profile=" + ((profile != null) ? profile : "");
if (!StringUtils.hasText(name)) {
loadIntoGroup(group, location, profile);
}
else {
// 支持的文件类型是properties,xml,yml等文件格式
for (String ext : this.propertiesLoader.getAllFileExtensions()) {
if (profile != null) {
loadIntoGroup(group, location + name + "-" + profile + "." + ext,
null);
for (Profile processedProfile : this.processedProfiles) {
if (processedProfile != null) {
loadIntoGroup(group, location + name + "-"
+ processedProfile + "." + ext, profile);
}
}
loadIntoGroup(group, location + name + "-" + profile + "." + ext,
profile);
}
// 加载配置文件
loadIntoGroup(group, location + name + "." + ext, profile);
}
}
}

再进入loadIntoGroup方法

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
private PropertySource<?> doLoadIntoGroup(String identifier, String location,
Profile profile) throws IOException {
Resource resource = this.resourceLoader.getResource(location);
PropertySource<?> propertySource = null;
StringBuilder msg = new StringBuilder();
if (resource != null && resource.exists()) {
String name = "applicationConfig: [" + location + "]";
String group = "applicationConfig: [" + identifier + "]";
propertySource = this.propertiesLoader.load(resource, group, name,
(profile != null) ? profile.getName() : null);
if (propertySource != null) {
msg.append("Loaded ");
handleProfileProperties(propertySource);
}
else {
msg.append("Skipped (empty) ");
}
}
else {
msg.append("Skipped ");
}
msg.append("config file ");
msg.append(getResourceDescription(location, resource));
if (profile != null) {
msg.append(" for profile ").append(profile);
}
if (resource == null || !resource.exists()) {
msg.append(" resource not found");
this.logger.trace(msg);
}
else {
this.logger.debug(msg);
}
return propertySource;
}

可以看到这里是通过this.propertiesLoader.load()方法读取配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public PropertySource<?> load(Resource resource, String group, String name,
String profile) throws IOException {
if (isFile(resource)) {
String sourceName = generatePropertySourceName(name, profile);
for (PropertySourceLoader loader : this.loaders) {
// 判断当前loader是否能读取该类型文件
if (canLoadFileExtension(loader, resource)) {
PropertySource<?> specific = loader.load(sourceName, resource,
profile);
addPropertySource(group, specific);
return specific;
}
}
}
return null;
}

然后又通过loader.load()方法执行具体的读取逻辑

1
2
3
4
5
6
public PropertySourcesLoader(MutablePropertySources propertySources) {
Assert.notNull(propertySources, "PropertySources must not be null");
this.propertySources = propertySources;
this.loaders = SpringFactoriesLoader.loadFactories(PropertySourceLoader.class,
getClass().getClassLoader());
}

结合META-INF/spring.factories,可以看出loaders实际上PropertiesPropertySourceLoaderYamlPropertySourceLoader对象。

我们看下PropertiesPropertySourceLoaderload方法。

1
2
3
4
5
6
7
8
9
10
public PropertySource<?> load(String name, Resource resource, String profile)
throws IOException {
if (profile == null) {
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
if (!properties.isEmpty()) {
return new PropertiesPropertySource(name, properties);
}
}
return null;
}

PropertiesLoaderUtils.loadProperties()方法将配置文件中的内容读取写入到Properties对象里,到这里已经执行了Spring Boot对配置文件内容的读取。

总结

整个源码分析的过程还是很长的,牵涉到很多的类,中间的关系还是有点复杂。不过如果第一遍看不懂的话,可以多看几遍,多跟踪几遍源码,每次你都会多明白一些原理,看到后面自然就明白其中的原理了。这里为了简化分析,忽略了其他一部分因素的影响,例如profile等。
虽然看源码的过程比较枯燥繁琐,且懂不懂源码对业务开发没有明显的影响,但是如果你想突破自己,提升自己,阅读优秀源码是一门必修的课程。

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×