SpringBoot外化配置源码解析:命令参数获取文件加载

1496 篇文章 10 订阅
1494 篇文章 14 订阅

命令参数的获取

命令行参数就是在启动 Spring Boot 项目时通过命令行传递的参数。比如,用如下命令来启动一个 Spring Boot 的项目。

java -jar app.jar --name=SpringBoot

那么,参数--name=SpringBoot 是如何一 步步传递到 Spring 内部的呢?这就是本节要分析的代码内容。

默认情况下,SpringApplication 会将以 上类似 name 的命令行参数(以“”开通)解析封装成一-个 PropertySource 对象 (5.2 节已经具体讲到),并将其添加到 Spring-Environment 当中,而命令行参数的优先级要高于其他配置源。

下面,我们通过代码来追踪启动过程中整个参数的获取、解析和封装过程。首先,参数是通过 SpringApplication 的 run 方法的 args 参数来传递的。

在 SpringApplication 的 run 方 法 中 , 通 过 以 下 操 作 先 将 args 封 装 于 对 象ApplicationArguments 中,然后又将封装之后的对象传递入 prepareEnvironment 方法。

public ConfigurableApplicationContext run(String... args) {
ApplicationArguments applicationArguments = new DefaultApplicat ionArgu-
ments(args);ConfigurableEnvironment environment = prepareEnvironment(listeners,
applicationArg
uments);
} catch (Throwable ex) {
}
}

在 prepareEnvironment 方法中通过 applicationArguments. getSourceArgs()获得传递的参数数组,并作为参数调用 configureEnvironment 方法,此处获得的 args 依旧是未解析的参数值,代码如下。

private ConfigurableEnvironment prepareEnvironment (
SpringApplicationRunL isteners listeners,
ApplicationArguments applicationArguments) {
configureEnvironment (environment, applicationArguments . getSourceArgs());
}
在 configureEnvironment 方法中又将参数传递给 configurePropertySources 方法。
protected void configureEnvironment (ConfigurableEnvironment environment,
String[] args) {
configurePropertySources(environment, args);
}
}

而在 configurePropertySources 方法中才对参数进行了真正的解析和封装。

protected void configurePropertySources(ConfigurableEnvironment environmeString[]
args) {
//获得环境中的属性资源信息
MutablePropertySources sources = environment . getPropertySources();
//如果默认属性配置存在,则将其放置在属性资源的最后位置
if (this. defaultProperties != null && !this . defaultProperties . isEmpty())
{
sources. addL ast (new MapPropertySource(" defaultProperties", this . default
Properties));
//如果命令行属性未被禁用且存在
if (this . addCommandL ineProperties && args.length > 0) {
String name = CommandL inePropertySource . COMMAND_ LINE_ PROPERTY_
SOURCE_
NAME;
//如果默礼属性资源中不包含该命令则将命令行属性放置在第一位
//如果包含则通过 Compos itePropertySource 进行处理
if (sources . contains(name)) {PropertySource<?> source = sources . get(name);
CompositePropertySource composite = new CompositePropertySource(nam
e);
composite . addPropertySource (new SimpleCommandL inePropertySource(
"springApplicationCommandL ineArgs", args));
composite . addPropertySource(source);
sources . replace(name, composite);
} else|
/不存在,则添加并放置在第一位
sources . addFirst(new SimpleCommandL inePropertySource(args));
}

configurePropertySources 方法在之前章节中有过讲解,下面针对命令行参数再次进行讲解和深入分析,重点介绍两个内容:参数的优先级和命令行参数的解析。

先说参数的优先级,从上面的代码注解中可以看到,configurePropertySources 方法第一步获得环境变量中存储配置信息的

sources;第二步判断默认参数是否为空,如果不为空,则将默认参数放置在 sources 的最后位置,这里已经明显反映了参数的优先级是通过顺序来体现的;第三步,如果命令参数未被禁用,且不为空,则要么将原有默认参数替换掉,要么直接放在第一位,这-一步中的替换操作也是另外一种优先级形式的体现。

顺便提一下, 在上面的代码中,addCommandL ineProperties 参数是可以进行设置的,当不允许使用命令行参数时,可以通过 SpringApplication 的
setAddCommandLineProperties方法将其设置为 false 来禁用。

命令行参数的解析用到了
SimpleCommandLinePropertySource 类,而该类的相关使用在上一节中已经详细介绍了。

通过上面一系列的代码追踪,我们了解了通过命令传递的参数是如何一步步被封装入 Spring的 Environment 当中的。下一 节,我们将分析配置文件中的参数获取。

配置文件的加载

Spring Boot 启动时默认会加载 classpath 下的 application.yml 或 application.properties 文件。配置文件的加载过程主要是利用 Spring Boot 的事件机制来完成的,也就是我们之前章节所讲到的 SpringApplicationRunL isteners 中的 environmentPrepared 方法来启动加载配置文件的事件。通过该方法发布的事件会被注册的
ConfigFileApplicationListener 监听到,从而实现资源的加载。

下面,我们通过源代码的追踪来分析这一过程。该事件同样是在 SpringApplication 的 run方法中来完成的。前半部分的调用过程与上一节命令行参数获取的方法调用一样,不同的是当执行到 prepareEnvironment 中,当执行完上一节中的 configureEnvironment 方法之后,便通过事件发布来通知监听器加载资源。

private ConfigurableEnvironment prepareEnvironment{
SpringApplicationRunL isteners listeners ,
ApplicationArguments applicationArguments) {
//获取或创建环境
ConfigurableEnvironment environment = getOrCreateEnvironment();
//配置环境,主要包括 PropertySources 和 activeProfiles 的配置
configureEnvironment( environment, applicat ionArguments . getSourceArgs());
// listener 环境准备(之前章节已经提到
listeners . environmentPrepared( environment);
}
}

该事件监听器通过
EventPublishingRunListener 的 environmentPrepared方法来发布一个 ApplicationEnvironmentPreparedEvent 事件。

public class EventPublishingRunL istener implements SpringApplicationRunList
ener ,
Ordered {
@Override
public void environmentPrepared(ConfigurableEnvironment environment) {
this. initialMulticaster. multicastEvent(new ApplicationEnvironmentPre-
paredEvent(
this. application, this.args, e
nvironment));
}

在 META-INF/spring .factories 中注册的
ConfigFileApplicationListener 会监听到对应事件,并进行相应的处理。spring.factories 中 ConfigFileApplicationListener 的注册配置如下。

# Application Listeners
org. springframework. context . ApplicationListener=\
org. springframework . boot . context . config. ConfigFileApplicationListener
在 ConfigFileApplicationListener 类中我们会看到很多与配置文件加载相关的常量。
public class ConfigFileApplicationListener
implements EnvironmentPostProcessor, SmartApplicationListener, Ordered {
//默认的加戴配置文件路径
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. ac
tive";
}

我们通过这些基本的常量,已经可以看出默认加载配置文件的路径和默认的名称了。再回到刚才的事件监听,入口方法为
ConfigFileApplicationListener 的 onApplicationEvent 方法。

@Override
public void onApplicationEvent (ApplicationEvent event) {
//对应前面发布的事件,执行此业务逻辑
if (event instanceof ApplicationEnvironmentPreparedEvent) {
onApplicationEnvironmentPreparedEvent( (Applicat ionEnvironmentPrepared-
Event) event);
if (event instanceof Applicat ionPreparedEvent) {
onApplicationPreparedEvent(event);
}

上面代码中调用的
onApplicationEnvironmentPreparedEvent 方法如下,该方法会获得注册的处理器,遍历并依次调用其 postProcessEnvironment 方法。

private void onApplicat ionEnvi ronmentPreparedEvent( Applicat ionEnvironmentPre -
paredEvent event) {
List<EnvironmentPostProcessor> postProcessors = loadPostProcessors();
postProcessors . add(this);
Annotat ionAwareOrderComparator . sort (postProcessors);
//遍历并依次调用其 postProcessEnvironment 方法
for (EnvironmentPostProcessor postProcessor : postProcessors) {
postProcessor. postProcessEnvironment ( event . getEnvironment( ) ,
event . getSpringApplication());
}

其中 EnvironmentPostProcessor 接口的实现类也是在 META-INF/sprig.factories 文件中注册的。

# Environment Post 处理器配置
org. springframework. boot . env. EnvironmentPostProcessor=\
org. springframework . boot . cloud . CloudF oundryVcapEnvironmentPostProcessor, \
org. springframework. boot . env. SpringApplicationJsonEnvironmentPostProcessor,\
org . springframework . boot. env . SystemEnvironmentPropertySourceEnvironmentPostProcessor


ConfigFileApplicationListener 本身也是 EnvironmentPostProcessor 接口的实现类我们跟着ConfigFileApplicationListener 中 postProcessEnvironment 的调用链路代码一-直往下看,会发现最后在其内部类 Loader 的 load 方法中进行配置文件的加载操作。其中关于文件路径及其名称的组合代码如下。

private void load(String location, String name, Profile profile,
DocumentFilterFactory filterFactory, DocumentConsumer con
sumer) {
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 的 IoadFactories 方法来获得。

# PropertySource 加载器配置
org. springframework. boot . env . PropertySourceLoader=\
org. springframework . boot . env . PropertiesPropertySourceLoader, \
org. springframework . boot . env. YamlPropertySourceLoader

当查看
PropertiesPropertySourceLoader 和 YamlPropertySourceLoader 两个加载器代码,就会发现它们分别定义了所支持文件类型及其加载方法。PropertiesPropertySourceL oader支持配置文件类型的定义代码如下。

public class PropertiesPropertySourceLoader implements PropertySourceLoader
private static final String XML_ FILE_ EXTENSION =”.xml";
@Override
public String[] getFileExtensions()
}}
return new String[] { " properties", "xml"YamlPropertySourceLoader 支持配置文件类型的
定义代码如下。
public class YamlPropertySourceLoader implements PropertySourceLoader {
@Override
public String[] getFileExtensions() {
return new String[] { "yml", "yaml" }
.}
}

其中
PropertiesPropertySourceLoader 对文件的加载通过 PropertiesLoaderUtils 类( 加载xml 文件)和 OriginTrackedPropertiesL oader 类来完成,而 YamlPropertySourceLoader 对文件的加载主要通过 OriginTrackedYamIL oader 来完成。

下面以
PropertiesPropertySourceLoader 使用的 OriginTrackedPropertiesL oader 为例进行源码分析。


PropertiesPropertySourceLoader 中加载相关的代码如下。

public class PropertiesPropertySourceLoader implements PropertySourceLoader
//加载指定的配置文件
@Override
public 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();
}
}

我们一起看以上代码中 properties 格式的加载,也就是最后一行代码的业务逻辑实现。这里创 建 了
OriginTrackedPropertiesLoader 对 象 并 调 用 了 其 load 方 法 。


OriginTrackedPropertiesLoader 的构造方法非常简单,只是把 resource 预置给其成员变量Resource resource。

再来重点看 load 方法的实现,代码如下。

class OriginTrackedPropertiesLoader {
private final Resource resource;
/**
* Load {@code . properties} data and return a map of {@code String} - >
* {@Link OriginTrackedValue}.
@param expandLists if list {@code name[]=a,b,c} shortcuts should be
expanded
@return the Loaded properties
@throws IOException on read error
*/
//加戴 properties 文件的数据并返 Emap 类型
//其中 expandLists 用于指定参数为"name[]=a, b,c"的列表是否进行扩展,默 itrue
Map<String, OriginTrackedValue> load(boolean expandLists) throws IOExcept
ion {
//创建配置文件的 reader
try (CharacterReader reader = new CharacterReader(this . resource)) {
Map<String, OriginTrackedValue> result = new LinkedHashMap<>();
StringBuilder buffer = new StringBuilder();
//读取文件中的数据
while (reader .read()) {
//读取文件中的 key
String key = loadKey(buffer, reader). trim();
/可扩展列表的处理
if (expandLists && key. endsWith("[]")) {
key = key. substring(0, key . length()- 2);
int index = 0do {
OriginTrackedValue value = loadValue(buffer, reader, true);
put(result, key + "[" + (index++) + "]",value);
if (!reader. isEndofLine()){
reader . read();
}
while (!reader . isEndOfLine());
} else
//读取文件中 value 并封装为 OriginTrackedValue
OriginTrackedValue value = loadValue(buffer, reader, false);
put(result, key, value);
return result;
}
}

以上代码展示了 OriginTrackedPropertiesL oader 的 load 方法的核心功能:创建 reader 读取配置文件、获得配置文件中配置的 key、 获取配置文件中的 value、封装 key-value 到 map中并返回。

关于 loadKey、loadValue 的操作无非就是字符串按照指定格式的解析,具体实现都在该类内部,就不附上代码了。

本节以 properties 类型的配置文件为例讲解了其解析加载过程是如何进行的,其他类型的操作过程基本一致,只不过不同文件的具体解析方式有所不同。因此,关于其他类型的代码解析就不在此深入拓展了,感兴趣的读者可以继续查看这两个类的其他源码进行了解。

本文给大家讲解的内容是命令参数的获取和配置文件的加载

  1. 下篇文章给大家讲解的是基于Profile的处理实现;
  2. 觉得文章不错的朋友可以转发此文关注小编;
  3. 感谢大家的支持!

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值