Spring Boot
是目前最流行的Java开发框架,它提供了很多的默认配置,不需要我们再去逐一配置,极大地简化了开发流程。项目中的部分具体配置值一般都写在application.properties
或application.yml
中,本文就让我们一起来探讨一下Spring Boot
如何加载配置文件中的内容。
本文针对有一定Spring Boot
使用基础的同学,才能更好地理解后面叙述的内容。
基于
Spring Boot 1.5.6.RELEASE
版本
首先让我们了解一下Spring Boot
简介(以下内容来自百度百科)
Spring Boot是由Pivotal团队提供的全新框架,其设计目的是用来简化新Spring应用的初始搭建以及开发过程。该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置。通过这种方式,Spring Boot致力于在蓬勃发展的快速应用开发领域(rapid application development)成为领导者。
Spring Boot
特性
- 约定优于配置,大多数的配置直接使用默认配置即可
- 快速搭建项目,脱离繁杂的
XML
配置 - 内嵌
Servlet
容器,不依赖外部容器 - 与云计算天然集成,提供主流框架一键集成方式
接下来,进入正题通过源码分析理解Spring Boot
加载配置文件内容的过程(部分方法内容没有贴出,可以根据源码一步一步跟踪下去)。
所有的Spring Boot
项目都是由SpringApplication.run()
这个静态方法开始执行的,源码分析也从这里开始进行。1
2
3public static ConfigurableApplicationContext run(Object source, String... args) {
return run(new Object[] { source }, args);
}
run()
方法内部实际上构造了一个SpringApplication
对象并执行对象的run()
方法(非静态方法)1
2
3public 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
13private 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
12private <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
21public 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
35public 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
5private 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 | public void onApplicationEvent(ApplicationEvent event) { |
这里可以看到执行了onApplicationEnvironmentPreparedEvent()
方法。首先加载通过SpringFactoriesLoader.loadFactories()
加载对应的Processor
,然后将ConfigFileApplicationListener
也添加到了postProcessors
中。原来它也实现了EnvironmentPostProcessor
接口。1
2
3public class ConfigFileApplicationListener
implements EnvironmentPostProcessor, SmartApplicationListener, Ordered {
}
紧接着执行每个Processor
的postProcessEnvironment()
方法。这里我们主要关注ConfigFileApplicationListener.postProcessEnvironment()
方法。1
2
3
4
5
6
7
8
9
10
11
12public 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
36public 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
22public 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
3for (String name : getSearchNames()) {
load(location, name, profile);
}
在进入getSearchNames()
方法1
2
3
4
5
6
7
8private 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.names
是null
,所以返回的就是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
25private 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
35private 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
16public 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
6public 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
实际上PropertiesPropertySourceLoader
和YamlPropertySourceLoader
对象。
我们看下PropertiesPropertySourceLoader
的load
方法。1
2
3
4
5
6
7
8
9
10public 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
等。
虽然看源码的过程比较枯燥繁琐,且懂不懂源码对业务开发没有明显的影响,但是如果你想突破自己,提升自己,阅读优秀源码是一门必修的课程。