Spring Boot外化配置源码解析
外化配置简介
Spring Boot设计了非常特殊的加载指定属性文件(PropertySouce)的顺序,允许属性值合理的覆盖,属性值会以下面的优先级进行配置。
- home目录下的Devtool全局设置属性(~/.spring-boot-devtools.properties,条件是当devtools激活时)
- @TestPropertySource注解的测试用例。
- @SpringBootTest#properties注解的测试用例。
- 命令行参数。
- 来自SPRING_APPLICATION_JSON的属性(内嵌在环境变量或系统属性中的内联JSON)
- ServletConfig初始化参数
- ServletContext初始化参数
- java:comp/env的JNDI属性
- Java系统属性(System.getProperties())
- 操作系统环境变量
- RandomValuePropertySource,只包含random.*中的属性
- jar包外的Profile_specific应用属性(application-{profile}.propertis和YAML变量)
- jar包内的Profile_specific应用属性(application-{profile}.propertis和YAML变量)
- jar包外的应用配置(application.properties和YAML变量)
- jar包内的应用配置(application.properties和YAML变量)
- @Configuration类上的@PropertySource注解
- 默认属性(通过SpringApplication.setDefaultProperties指定)
在以上配置方式中,我们经常使用的包括:命令参数,属性文件,YAML文件等内容,以下将围绕他们的运行及相关代码进行讲解。
ApplicationArguments参数处理
ApplicationArguments提供了针对参数的解析和查询功能。在Spring Boot运行阶段的章节中我们提到过,通过SpringApplication.run(args)传递的参数会被封装在ApplicationArguments接口中。本节我们来详细了解下ApplicationArguments接口。
接口定义及初始化
首先看一下ApplicationArguments接口的具体方法定义及功能介绍。
package org.springframework.boot;import java.util.List;
import java.util.Set;public interface ApplicationArguments {//返回原始未处理的参数(通过application传入的)String[] getSourceArgs();//返回所有参数的集合,如参数为:--foo=bar --debug,则返回【"foo","debug"】Set<String> getOptionNames();//选项参数中是否包含指定名称的参数boolean containsOption(String name);//根据选项参数的名称获取选项参数的值列表List<String> getOptionValues(String name);//返回非选项参数列表List<String> getNonOptionArgs();}
通过接口定义可以看出,ApplicationArguments主要提供了针对参数名称和值的查询,以及判断是否存在指定参数的功能。
在Spring Boot的初始化运行过程中,ApplicationArguments接口的实例化操作默认是通过实现类DefaultApplicationArguments来完成的。DefaultApplicationArguments的底层又是基于Spring框架中的命令行配置源SimpleCommandLinePropertySource实现的,SpringCommandLinePropertySource是PropertySource抽象类的派生类。
以下代码中内部类Source便是SimppleCommandLinePropertySource的子类。
public class DefaultApplicationArguments implements ApplicationArguments {private final Source source;private final String[] args;public DefaultApplicationArguments(String... args) {Assert.notNull(args, "Args must not be null");this.source = new Source(args);this.args = args;}......private static class Source extends SimpleCommandLinePropertySource {......}}
我们再来看SimpleCommandLinePropertySource的构造方法,通过代码会发现默认使用spring的SimpleCommandLineArgsParser对args参加进行解析。
public class SimpleCommandLinePropertySource extends CommandLinePropertySource<CommandLineArgs> {public SimpleCommandLinePropertySource(String... args) {super((new SimpleCommandLineArgsParser()).parse(args));}//重载的构造方法public SimpleCommandLinePropertySource(String name, String[] args) {super(name, (new SimpleCommandLineArgsParser()).parse(args));}......
}
除了构造方法之外,SimpleCommandLinePropertySource还提供了不同类型参数信息的获取和检查是否存在的功能,代码如下:
public class SimpleCommandLinePropertySource extends CommandLinePropertySource<CommandLineArgs> {......//获取选项参数数组 public String[] getPropertyNames() {return StringUtils.toStringArray(((CommandLineArgs)this.source).getOptionNames());}//获取是否包含指定name的参数protected boolean containsOption(String name) {return ((CommandLineArgs)this.source).containsOption(name);}//获取指定name的选项参数列表@Nullableprotected List<String> getOptionValues(String name) {return ((CommandLineArgs)this.source).getOptionValues(name);}//获取非选项参数列表protected List<String> getNonOptionArgs() {return ((CommandLineArgs)this.source).getNonOptionArgs();}
}
ApplicatinArguments,或者更进一步说是SimpleCommandLinePropertySource对参数类型是有所区分的,即选项参数和非选项参数。
选项参数必须以“–”为前缀,参数值可为空,该参数可以通过Spring Boot属性处理后使用,比如在执行jar -jar命令时,添加选项参数“–app.name=spring boot start",在代码中可以通过注解@Value属性及其他方式获取到该参数的值。该参数可以通过逗号分隔多个参数值,或者多次使用同一个参数来包含多个参数的值。
非选项参数并不要求以“–”前缀开始,可自行定义。非选项参数可以直接在jar -jar命令中定义参数为“non-option"的参数值。
以上所说的选项参数和非选项参数的解析是在SimpleCommandLinePropertySource构造方法中调用SimpleCommandLineArgsParser中完成的,代码如下:
class SimpleCommandLineArgsParser {SimpleCommandLineArgsParser() {}//解析args参数,返回一个完整的CommandLineArgs对象public CommandLineArgs parse(String... args) {CommandLineArgs commandLineArgs = new CommandLineArgs();String[] var3 = args;int var4 = args.length;//遍历参数for(int var5 = 0; var5 < var4; ++var5) {String arg = var3[var5];//解析选项参数,以"--"开头if (arg.startsWith("--")) {String optionText = arg.substring(2, arg.length());String optionValue = null;String optionName;//判断是--foo=bar参数格式,还是-foo参数格式,并分别处理获取值if (optionText.contains("=")) {optionName = optionText.substring(0, optionText.indexOf(61));optionValue = optionText.substring(optionText.indexOf(61) + 1, optionText.length());} else {optionName = optionText;}if (optionName.isEmpty() || optionValue != null && optionValue.isEmpty()) {throw new IllegalArgumentException("Invalid argument syntax: " + arg);}commandLineArgs.addOptionArg(optionName, optionValue);} else {//处理非选项参数commandLineArgs.addNonOptionArg(arg);}}return commandLineArgs;}
}
通过SimpleCommandLineArgsParser的代码可以看出,Spring对参数的解析是按照指定的参数格式分别解析字符串中的值来实现的。最终,解析的结果均封装在CommandLineArgs中。而CommandLineArgs类只是命令行参数的简单表示形式,内部分为“选项参数”和"非选项参数"
class CommandLineArgs {private final Map<String, List<String>> optionArgs = new HashMap();private final List<String> nonOptionArgs = new ArrayList();CommandLineArgs() {}......
}
CommandLineArgs的核心存储结构包括:存储选项参数的Map<String,List>optionArgs和存储非选项参数的ListnonOptionsArgs。同时,针对这两个核心存储接口,SpringBoot也提供了相关的读写操作的方法。
SimpleCommandLineArgsParser解析获得的CommandLineArgs对象,最终会被SimpleCommandLinePropertySource的构造方法通过parser调用,一层层地传递到PropertySource类的构造方法中,最终封装到相应的属性当中。
public abstract class PropertySource<T> {protected final Log logger;//参数类别名称protected final String name;//参数封装类protected final T source;......
}
以在SimpleCommandLinePropertySource中的使用为例,最终封装在PropertySource中的结构为:name为“commandLineArgs”,source为解析出的CommandLineArgs对象。
而DefaultApplicationArguments的内部类Source作为SimpleCommandLinePropertySource的子类存储了以上解析的数据内容。同时,args参数的原始值储存在DefaultApplicationArguments的String[]args属性中。
命令行参数的获取
命令行参数就是在启动Spring Boot项目时通过命令行传递的参数。比如通过一下命令来启动一个Spring Boot项目。
jar -jar app.jar --name=SpringBoot
那么–name=SpringBoot是如何一步步传递到Spring内部的呢?
默认情况下,SpringApplication会将以上类似name的命令行参数(以“–”开头)解析封装成一个PropertySource对象,并将其添加到Spring-Environment当中,而命令行参数的优先级要高于其他配置源。
下面我们来通过代码来追踪启动过程中整个参数的获取,解析和分装过程。首先,参数是通过SpringApplication的run方法的args传递参数。
在SpringApplication的run方法中,通过以下操作先将args封装于ApplicationArguments中,然后又将封装之后的对象传递入prepareEnvironment方法。
public ConfigurableApplicationContext run(String... args) {......try {ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);......}catch (Throwable ex) {handleRunFailure(context, ex, exceptionReporters, listeners);throw new IllegalStateException(ex);}......}
在prepareEnvironment方法中,通过applicationArguments.getSourceArgs()获得传递的参数数组,并作为参数调用configureEnvironment方法,此处获得的args依旧是未解析的参数值,代码如下:
private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,ApplicationArguments applicationArguments) {......configureEnvironment(environment, applicationArguments.getSourceArgs());......}
在configureEnvironment方法中又将参数传递给configurePropertySource方法。
protected void configureEnvironment(ConfigurableEnvironment environment, String[] args) {......configurePropertySources(environment, args);configureProfiles(environment, args);}
而在configurePropertySources方法中才对参数进行了真正的解析和封装
protected void configurePropertySources(ConfigurableEnvironment environment, String[] args) {//获取环境变量中的属性资源信息MutablePropertySources sources = environment.getPropertySources();//如果默认属性配置存在,则将其放置在属性资源的最后位置if (this.defaultProperties != null && !this.defaultProperties.isEmpty()) {sources.addLast(new MapPropertySource("defaultProperties", this.defaultProperties));}//如果命令行属性未被禁用且存在if (this.addCommandLineProperties && args.length > 0) {String name = CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME;//如果默认属性资源中不包含该命令则将命令行属性放置在第一位//如果包含则通过CompositePropertySource进行处理if (sources.contains(name)) {PropertySource<?> source = sources.get(name);CompositePropertySource composite = new CompositePropertySource(name);composite.addPropertySource(new SimpleCommandLinePropertySource("springApplicationCommandLineArgs", args));composite.addPropertySource(source);sources.replace(name, composite);}else {//不存在,则添加并放置在第一位sources.addFirst(new SimpleCommandLinePropertySource(args));}}}
因为configurePropertySources方法在之前章节中介绍过,下面针对命令行参数再次进行讲解和深入分析,重点介绍两个内容:参数的优先级和命令行参数的解析。
参数的优先级,从上面的代码注解中可以看到,configurePropertySources方法
- 第一步获得环境变量中存储配置信息的sources;
- 第二步判断默认参数是否为空,如果不为空,则将默认参数放置在sources的最后位置,这里已经明显反应了参数的优先级是通过顺序来体现的;
- 第三步如果命令行参数未被禁用,且不为空,则要么将原有默认参数替换掉,要么直接放在第一位,这一步中的替换操作也是另外一种优先级形式的体现。
在上面代码中,可以通过SpringApplication的setAddCommandLineProperties方法将其设置为false来禁用。命令行参数的解析用到了SimpleCommandLinePropertySource类,而该类的相关使用在上面以详细介绍过。下面将分析配置文件中的参数获取。
配置文件的加载
Spring Boot启动时默认会去加载classpah下的application.yml或application.properties文件。配置文件的加载过程中主要是利用了Spring Boot的事件机制来完成的,也就是我们之前说的SpringApplicationRunListeners中的environmentPrepared方法来启动加载配置文件的事件。通过该方法发布的事件会被注册到ConfigFileApplicationListener监听到,而实现资源的加载。
下面来通过源码的追踪分析这一过程。该事件同样是SpringApplication的run方法来完成的。前半部分的调用过程与上面命令行获取参数的方法调用一样,不同的是当执行到prepareEnvironment中,当执行完configureEnvironment方法之后,便通过事件发布来通知监听器加载资源。
private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,ApplicationArguments applicationArguments) {// Create and configure the environmentConfigurableEnvironment environment = getOrCreateEnvironment();// 配置环境,主要包括PropertySources和activeProfiles的配置configureEnvironment(environment, applicationArguments.getSourceArgs());ConfigurationPropertySources.attach(environment);//listeners环境准备listeners.environmentPrepared(environment);......}
该事件监听器通过EventPublishingRunListener的environmentPrepared方法发布一个ApplicationEnvironmentPreparedEvent事件
public class EventPublishingRunListener implements SpringApplicationRunListener, Ordered {......@Overridepublic void environmentPrepared(ConfigurableEnvironment environment) {this.initialMulticaster.multicastEvent(new ApplicationEnvironmentPreparedEvent(this.application, this.args, environment));}......
}
在META-INF/spring.factories中注册的ConfigFileApplicationListener会监听到对应事件,并进行相应的处理。spring.factories中ConfigFileApplicationListener的注册配置如下:
# 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.context.logging.ClasspathLoggingApplicationListener,\
org.springframework.boot.context.logging.LoggingApplicationListener,\
org.springframework.boot.liquibase.LiquibaseServiceLocatorApplicationListener
在ConfiFileApplicationListener类中我们会看到很多与配置文件加载相关的常量。
public class ConfigFileApplicationListener implements EnvironmentPostProcessor, SmartApplicationListener, Ordered {private static final String DEFAULT_PROPERTIES = "defaultProperties";// 默认的加载配置文件路径private static final String DEFAULT_SEARCH_LOCATIONS = "classpath:/,classpath:/config/,file:./,file:./config/";//默认的配置文件名称private static final String DEFAULT_NAMES = "application";......//激活配置文件的属性名public static final String ACTIVE_PROFILES_PROPERTY = "spring.profiles.active";......
通过这些基本的常量,可以看出默认加载配置文件的路径和默认的名称。再回到刚才的事件监听,入口方法为ConfigFileApplicationListener的onApplicationEvent方法。
@Overridepublic void onApplicationEvent(ApplicationEvent event) {//对应当前发布的事件,执行次业务逻辑if (event instanceof ApplicationEnvironmentPreparedEvent) {onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent) event);}if (event instanceof ApplicationPreparedEvent) {onApplicationPreparedEvent(event);}}
上面调用onApplicationEnvironmentPreparedEvent方法如下,该方法会获得注册的处理器,遍历并依次调用postPropcessEnvironment方法。
private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {List<EnvironmentPostProcessor> postProcessors = loadPostProcessors();postProcessors.add(this);AnnotationAwareOrderComparator.sort(postProcessors);//遍历并依次调用postProcessEnvironment方法for (EnvironmentPostProcessor postProcessor : postProcessors) {postProcessor.postProcessEnvironment(event.getEnvironment(), event.getSpringApplication());}}
其中EnvironmentPostProcessor接口的实现类也是在META-INF/spring.factories文件中注册的。
# Environment Post Processors
org.springframework.boot.env.EnvironmentPostProcessor=\
org.springframework.boot.cloud.CloudFoundryVcapEnvironmentPostProcessor,\
org.springframework.boot.env.SpringApplicationJsonEnvironmentPostProcessor,\
org.springframework.boot.env.SystemEnvironmentPropertySourceEnvironmentPostProcessor,\
org.springframework.boot.reactor.DebugAgentEnvironmentPostProcessor
ConfigFileApplicationListener本身也是EvironmentPostProcessor接口的实现类,可以跟着ConfigFileApplicationListener中postProcessEnvironment的调用链路代码一直往下看,会发现最后在其内部类Loader的load方法进行配置文件的加载操作。其中关于文件路径及其名称的组合代码如下:
// 1接口类,查找实现类ConfiFileApplicationListener
public interface EnvironmentPostProcessor {void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application);
}// 2 ConfiFileApplicationListener实现类
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {addPropertySources(environment, application.getResourceLoader());
}
// 3 addPropertySources
protected void addPropertySources(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {RandomValuePropertySource.addToEnvironment(environment);new Loader(environment, resourceLoader).load();
}
// 4 load()
void load() {FilteredPropertySource.apply(this.environment, DEFAULT_PROPERTIES, LOAD_FILTERED_PROPERTY,(defaultProperties) -> {this.profiles = new LinkedList<>();this.processedProfiles = new LinkedList<>();this.activatedProfiles = false;this.loaded = new LinkedHashMap<>();initializeProfiles();while (!this.profiles.isEmpty()) {Profile profile = this.profiles.poll();if (isDefaultProfile(profile)) {addProfileToEnvironment(profile.getName());}load(profile, this::getPositiveProfileFilter,addToLoaded(MutablePropertySources::addLast, false));this.processedProfiles.add(profile);}load(null, this::getNegativeProfileFilter, addToLoaded(MutablePropertySources::addFirst, true));addLoadedPropertySources();applyActiveProfiles(defaultProperties);});
}
// 5 load()
private void load(Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) {getSearchLocations().forEach((location) -> {boolean isFolder = location.endsWith("/");Set<String> names = isFolder ? getSearchNames() : NO_SEARCH_NAMES;names.forEach((name) -> load(location, name, profile, filterFactory, consumer));});
}
private void load(String location, String name, Profile profile, DocumentFilterFactory filterFactory,DocumentConsumer consumer) {......Set<String> processed = new HashSet<>();for (PropertySourceLoader loader : this.propertySourceLoaders) {for (String fileExtension : loader.getFileExtensions()) {if (processed.add(fileExtension)) {loadForFileExtension(loader, location + name, "." + fileExtension, profile, filterFactory,consumer);}}}}
在该方法中可以看到loadForFileExtension的第二个参数"文件路径+名称"和第三个参数"扩展名称"的拼接组成方式。location默认值就是常量DEFAULT_SEARCH_LOCATIONS的值。
在for循环中遍历的PropertySourceLoader也是在META-INF/spring.factories中注册的,并且在Loader的构造方法中通过SpringFactoriesLoader的loadFactories方法来获得。
# PropertySource Loaders
org.springframework.boot.env.PropertySourceLoader=\
org.springframework.boot.env.PropertiesPropertySourceLoader,\
org.springframework.boot.env.YamlPropertySourceLoader
当查看PropertiesPropertySourceLoader和YamlPropertySourceLoader两个加载器代码时,就会发现他们分别定义了所支持文件类型及其加载方法。PropertiesPropertySourceLoader支持配置文件类型的定义代码如下:
public class PropertiesPropertySourceLoader implements PropertySourceLoader {private static final String XML_FILE_EXTENSION = ".xml";@Overridepublic String[] getFileExtensions() {return new String[] { "properties", "xml" };}@Overridepublic List<PropertySource<?>> load(String name, Resource resource) throws IOException {Map<String, ?> properties = loadProperties(resource);if (properties.isEmpty()) {return Collections.emptyList();}return Collections.singletonList(new OriginTrackedMapPropertySource(name, Collections.unmodifiableMap(properties), true));}@SuppressWarnings({ "unchecked", "rawtypes" })private Map<String, ?> loadProperties(Resource resource) throws IOException {String filename = resource.getFilename();if (filename != null && filename.endsWith(XML_FILE_EXTENSION)) {return (Map) PropertiesLoaderUtils.loadProperties(resource);}return new OriginTrackedPropertiesLoader(resource).load();}}
YamlPropertySourceLoader支持配置文件类型的定义代码如下:
public class YamlPropertySourceLoader implements PropertySourceLoader {@Overridepublic String[] getFileExtensions() {return new String[] { "yml", "yaml" };}@Overridepublic List<PropertySource<?>> load(String name, Resource resource) throws IOException {if (!ClassUtils.isPresent("org.yaml.snakeyaml.Yaml", null)) {throw new IllegalStateException("Attempted to load " + name + " but snakeyaml was not found on the classpath");}List<Map<String, Object>> loaded = new OriginTrackedYamlLoader(resource).load();if (loaded.isEmpty()) {return Collections.emptyList();}List<PropertySource<?>> propertySources = new ArrayList<>(loaded.size());for (int i = 0; i < loaded.size(); i++) {String documentNumber = (loaded.size() != 1) ? " (document #" + i + ")" : "";propertySources.add(new OriginTrackedMapPropertySource(name + documentNumber,Collections.unmodifiableMap(loaded.get(i)), true));}return propertySources;}}
其中PropertiesPropertySourceLoader对文件的加载通过PropertiesLoaderUtils类(加载xml文件)和OriginTrackedYamlLoader类来完成,而YamlPropertySourceLoader对文件的加载主要通过OriginrackedYamlLoader来完成。
下面以PropertiesPropertySourceLoader使用的OriginTrackedPropertiesLoader为例进行源码分析。
PropertiesPropertySourceLoader中加载相关的代码如下:
public class PropertiesPropertySourceLoader implements PropertySourceLoader {private static final String XML_FILE_EXTENSION = ".xml";@Overridepublic String[] getFileExtensions() {return new String[] { "properties", "xml" };}//加载指定的配置文件@Overridepublic List<PropertySource<?>> load(String name, Resource resource) throws IOException {//调用load方法进行加载并返回Map形式的数据Map<String, ?> properties = loadProperties(resource);if (properties.isEmpty()) {return Collections.emptyList();}//对返回结果进行处理和转换return Collections.singletonList(new OriginTrackedMapPropertySource(name, Collections.unmodifiableMap(properties), true));}//具体加载过程@SuppressWarnings({ "unchecked", "rawtypes" })private Map<String, ?> loadProperties(Resource resource) throws IOException {String filename = resource.getFilename();//加载xml格式if (filename != null && filename.endsWith(XML_FILE_EXTENSION)) {return (Map) PropertiesLoaderUtils.loadProperties(resource);}//加载properties格式return new OriginTrackedPropertiesLoader(resource).load();}}
OriginTrackedPropertiesLoader的构造方法非常简单,只是把resource设置给其成员变量Resource。
class OriginTrackedPropertiesLoader {private final Resource resource;OriginTrackedPropertiesLoader(Resource resource) {Assert.notNull(resource, "Resource must not be null");this.resource = resource;}Map<String, OriginTrackedValue> load() throws IOException {return load(true);}//加载properties文件的数据并返回map类型//其中expandLists用于指定参数为"name[]=a,b,c"的列表是否进行扩展,默认为trueMap<String, OriginTrackedValue> load(boolean expandLists) throws IOException {//创建配置文件的readertry (CharacterReader reader = new CharacterReader(this.resource)) {Map<String, OriginTrackedValue> result = new LinkedHashMap<>();StringBuilder buffer = new StringBuilder();//读取文件中数据while (reader.read()) {//读取文件中的keyString key = loadKey(buffer, reader).trim();//可扩展列表的处理if (expandLists && key.endsWith("[]")) {key = key.substring(0, key.length() - 2);int index = 0;do {OriginTrackedValue value = loadValue(buffer, reader, true);put(result, key + "[" + (index++) + "]", value);if (!reader.isEndOfLine()) {reader.read();}}while (!reader.isEndOfLine());}else {//读取文件中value并封装为OriginTrackedValueOriginTrackedValue value = loadValue(buffer, reader, false);put(result, key, value);}}return result;}}
}
以上代码展示了OriginTrackedPropertiesLoader的load方法的核心功能:创建reader读取配置文件,获得配置文件中配置的key,获取配置文件中的value,封装key-value到map中并返回。