SpringBoot2 Binder的使用
Binder的使用其实比较简单 有点类似注解ConfigurationProperties的作用,都是将属性绑定到某个具体的对象上。 但是有一点区别 ConfigurationProperties是在容器启动时绑定的,而Binder是我们手动编码动态的绑定上去的。
我们回顾上一节 在向容器发送ApplicationEnvironmentPreparedEvent事件之后还执行了一行代码 bindToSpringApplication(environment) 下面我们看一下这行代码的具体作用
private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,ApplicationArguments applicationArguments) {
ConfigurableEnvironment environment = getOrCreateEnvironment();
configureEnvironment(environment, applicationArguments.getSourceArgs());
listeners.environmentPrepared(environment);
bindToSpringApplication(environment);
if (!this.isCustomEnvironment) {
environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment,deduceEnvironmentClass());
}
ConfigurationPropertySources.attach(environment);
return environment;
}
展开bindToSpringApplication方法
protected void bindToSpringApplication(ConfigurableEnvironment environment) {
try {
Binder.get(environment).bind("spring.main", Bindable.ofInstance(this));
}
catch (Exception ex) {
throw new IllegalStateException("Cannot bind to SpringApplication", ex);
}
}
以上代码是将spring.main下面的配置绑定到SpringApplication对象上。如:sources ,bannerMode等属性赋值给当前的对象。也就是将spring.main.sources 绑定到SpringbootApplication的sources属性上 将spring.main.banner-mode绑定到bannerMode属性上。可以理解为将属性动态绑定到对象上。
我们再看一处Springboot中动态绑定的代码
private List<Document> asDocuments(List<PropertySource<?>> loaded) {
if (loaded == null) {
return Collections.emptyList();
}
return loaded.stream().map((propertySource) -> {
Binder binder = new Binder(ConfigurationPropertySources.from(propertySource),
this.placeholdersResolver);
return new Document(propertySource, binder.bind("spring.profiles", STRING_ARRAY).orElse(null),
getProfiles(binder, ACTIVE_PROFILES_PROPERTY), getProfiles(binder, INCLUDE_PROFILES_PROPERTY));
}).collect(Collectors.toList());
}
这段代码也是在上一篇分析的ConfigFileApplicationListener中。将YamlPropertySourceLoader解析的List<PropertySource<?>> 包装成Document的过程。
代码的作用是将spring.profiles下面的配置解析成字符串数组 赋值给Document的profiles属性的过程。
分块配置
springboot 官网文档解释:You can specify multiple profile-specific YAML documents in a single file by using a spring.profiles key to indicate when the document applies
大概的意思是 可以使用 spring.profiles 的key在单个文件中指定多个特定 profile 的 YAML 文档,以指示文档何时应用
我们用一个demo演示一下
#模块一
server:
add: 192.168.1.100
spring:
profiles:
active:
- production
- eu-central
---
#模块二
spring:
profiles: development
server:
add: 127.0.0.1
---
#模块三
spring:
profiles: production & eu-central
server:
add: 192.168.1.120
在一个application.yml文件中 用 — 符号隔离每个模块 可以为每个模块设置加载条件。例如模块二的加载条件是当development被激活时 server.add才有效。模块三的激活条件是 production和eu-central同时被激活时才会输出192.168.1.120
我们运行程序输出server.add 这个时候输出的是192.168.1.120。假如我们注释掉模块一的spring.profiles.active 则输出 192.168.1.100
再看SpringBoot解析配置块的过程
上一篇文章我们只是分析了加载配置文件的流程,至于后面怎么去加载主配置文件里面配置的spring.profiles.active 和spring.profiles.include只是简单的带过。下面接着上一节的内容分析
在分析之前我们先看一下 配置文件的配置
#application.yml文件
server:
add: 192.168.1.100
spring:
profiles:
active: development
---
spring:
profiles: development
server:
add: 127.0.0.1
name:
test: 11111
---
spring:
profiles: production & eu-central
server:
add: 192.168.1.120
#application-development.yml 文件
name:
test: 2222
spring:
profiles:
active: test
#application-test.yml 文件
name:
test: 3333
以上有三个文件 application.yml 中采用了分块配置 并且指定了spring.profiles.active=development
在application-development.yml文件中指定了spring.profiles.active=test
1,加载application.yml
直接进入解析配置文件的方法 同样删掉了参数判断的代码
private void load(PropertySourceLoader loader, String location, Profile profile, DocumentFilter filter,DocumentConsumer consumer) {
Resource resource = this.resourceLoader.getResource(location);
String name = "applicationConfig: [" + location + "]";
//这里第一次加载application.yml文件 图1 处是解析成document之后的值。这里解析出三个document。 因为我们是分块配置
List<Document> documents = loadDocuments(loader, name, resource);
List<Document> loaded = new ArrayList<>();
for (Document document : documents) {
if (filter.match(document)) {
//将spring.profiles.active配置的内容加入到profiles 用于以后解析
//这里有一个条件 当加入完从成之后会将 activatedProfiles属性改为true 第二次加入的时候会先判断activatedProfiles值 如果为true 不会再加入。
//所以到这 我们知道application-development中配置的 spring.profiles.active=test就不会被加入 所以文件application-test.yml不会被解析
addActiveProfiles(document.getActiveProfiles());
addIncludedProfiles(document.getIncludeProfiles());
loaded.add(document);
}
}
}
图1:
上面会对解析的三个文档做一个过滤 继续跟进filter的match方法
//加载application.yml时 当前的profile是null 所以第一次加载只会进入第一次if分支中
private DocumentFilter getPositiveProfileFilter(Profile profile) {
return (Document document) -> {
if (profile == null) {
//这里判断如果document的profiles为null 才会执行将解析的内容加入loaded缓存中
return ObjectUtils.isEmpty(document.getProfiles());
}
return ObjectUtils.containsElement(document.getProfiles(), profile.getName())&& this.environment.acceptsProfiles(Profiles.of(document.getProfiles()));
};
}
所以根据上述的逻辑 application.yml文件中的三个模块 第一次只会加载第一个模块里面的内容。并将development加入到profiles中。等待下次while循环解析。
2,加载active配置文件
public void load() {
....
while (!this.profiles.isEmpty()) {
//这一次取出的是 上一次解析application.yml的spring.profiles.active属性配置的development
Profile profile = this.profiles.poll();
if (profile != null && !profile.isDefaultProfile()) {
//将development加入Environment的activeProfiles属性中
addProfileToEnvironment(profile.getName());
}
//继续加载application-development.yml配置文件
load(profile, this::getPositiveProfileFilter, addToLoaded(MutablePropertySources::addLast, false));
this.processedProfiles.add(profile);
}
....
}
我们看到上面和加载application.yml的流程是一样的。下面直接进入到 具体的加载配置的方法。
private void loadForFileExtension(PropertySourceLoader loader, String prefix, String fileExtension,Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
DocumentFilter defaultFilter = filterFactory.getDocumentFilter(null);
DocumentFilter profileFilter = filterFactory.getDocumentFilter(profile);
//上一节我们提到 如果profile不为空是直接进入到if分支里面的
if (profile != null) {
//如果profile不为空 拼接 文件名字
String profileSpecificFile = prefix + "-" + profile + fileExtension;
//加载内容 注意这里传的filter是filterFactory.getDocumentFilter(null)获取的filter
//所以这里还是会和解析application.yml的流程一样。会把application-development.yml解析结果放在loaded中
//如果application-development.yml是分块配置 会将默认(没有配置spring.profiles值的块)的块加入loaded中
load(loader, profileSpecificFile, profile, defaultFilter, consumer);
//这里再次解析的目的是如果 application-development.yml是分块配置 会把 spring.profiles=development的块配置的内容解析加载到loaded中
load(loader, profileSpecificFile, profile, profileFilter, consumer);
//这里的 this.processedProfiles是我们处理过的集合
for (Profile processedProfile : this.processedProfiles) {
//过滤掉profile = null的 也就是 application.yml
if (processedProfile != null) {
//和上面过程一样 重新拼接 文件名字
String previouslyLoaded = prefix + "-" + processedProfile + fileExtension;
//再一次加载之前加载过的 配置文件 请注意这里传的是一个profileFilter 而不是默认的filter
// 从刚在我们的分析知道默认的 实际上默认的filter本质上是加载默认块的内容 而profileFilter是加载指定块的内容
//哪这里为什么又一次遍历加载呢?其实原因也很简单 假如我么的application.yaml 中 spring.profiles.active指定了2个值 development,test
//而在development的配置文件中指定了 test模块 所以这里要把之前没有加载的test模块加载到loaded中
load(loader, previouslyLoaded, profile, profileFilter, consumer);
}
}
}
//查找主配置文件(application.yml)里面的 当前激活的配置块
load(loader, prefix + fileExtension, profile, profileFilter, consumer);
}
上面的代码主要是兼容 配置文件中的配置块的配置。加载过程比较复杂。这里我们整理一下
假设我们有三个配置文件:
application.yml 内容
server:
add: 192.168.1.100
spring:
profiles:
active: development,test
---
spring:
profiles: development
server:
add: 127.0.0.1
---
spring:
profiles: test
server:
add: 192.168.1.120
application.yml 有三个配置块 其中在主配置块中指定了 激活的配置块为development和test
application-development.yml配置文件内容
server:
add: 192.168.2.100
---
spring:
profiles: test
server:
add: 192.168.2.300
develpment配置文件有两个模块 一个是主模块 一个是test模块
application-test.yml的配置文件内容
server:
add: 192.168.3.100
---
spring:
profiles: development
server:
add: 192.168.3.200
test文件中有两个模块 一个是主模块一个是 development模块
当启动程序加载以上三个配置文件的时候 会执行以下的顺序:
- 加载application.yml文件的主模块的内容
- 将application.yml中的spring.profiles.active 放入待解析的集合中
- 加载application-development.yml文件并解析主模块内容
- 解析application-development.yml文件的development模块内容
- 解析application.yml中development模块的内容
- 加载application-test.yml文件 并解析主模块内容
- 解析application-test.yml文件中的test模块内容
- 解析application-development.yml文件中test模块中的内容
- 解析application.yml文件中test模块中的内容
从以上的加载规则可以看到 如配置的spring.profiles.active=development,test 那么test文件中的development模块是无法被加载的。
配置文件优先级
所谓配置文件优先级 其实是指配置文件的在MutablePropertySources中的顺序。下标小的会被提前遍历 如果条件匹配 提前返回 所以就没有后面的配置什么事了。我们首先看一下加载配置文件是怎么被添加到Environment的容器MutablePropertySources中的。
private void addLoadedPropertySources() {
MutablePropertySources destination = this.environment.getPropertySources();
//将解析的LinkedhashMap 的value转成list 注意这里是有序的LinkedHashMap 而不是hashmap
//所以根据我们上面的解析逻辑 application.yml最先被解析 放在第一个development再次被解析放在第二个 test放在最后
List<MutablePropertySources> loaded = new ArrayList<>(this.loaded.values());
//这里对list做了一个位置翻转。也就是 现在的顺序变成 test development application.yml
Collections.reverse(loaded);
String lastAdded = null;
Set<String> added = new HashSet<>();
//这里遍历上面经过翻转的集合 一个个添加到Environment 我们看一下下面的代码是怎么添加的
for (MutablePropertySources sources : loaded) {
for (PropertySource<?> source : sources) {
if (added.add(source.getName())) {
addLoadedPropertySource(destination, lastAdded, source);
lastAdded = source.getName();
}
}
}
}
//添加逻辑
private void addLoadedPropertySource(MutablePropertySources destination, String lastAdded,PropertySource<?> source) {
if (lastAdded == null) {
if (destination.contains(DEFAULT_PROPERTIES)) {
destination.addBefore(DEFAULT_PROPERTIES, source);
}
else {
//第一次进入这里 吧test放在最后面
destination.addLast(source);
}
}
else {
//后面会进入这里 因为lastAdded有值了 lastAdded保存的是上一次添加元素的值 这一个操作会把
//当前的元素放在上一个添加元素的后面。详细操作可以看 下面的源码
destination.addAfter(lastAdded, source);
}
}
public void addAfter(String relativePropertySourceName, PropertySource<?> propertySource) {
assertLegalRelativeAddition(relativePropertySourceName, propertySource);
removeIfPresent(propertySource);
//找到上一次添加元素的位置
int index = assertPresentAndGetIndex(relativePropertySourceName);
//直接将元素放到index+1的位置
addAtIndex(index + 1, propertySource);
}
从上面我们了解到application.yml的优先级对于我们配置来说是最低。然后就是我们配置的
spring.profiles.active 如果有多个值 越靠后优先级越高。
下面我们附一张图 所有配置文件在Environment中的顺序。
上图可以看到 除了我们的自定义配置文件。其他配置项的优先级分别是:
commandLine > servletConfigInitParams > servletContextInitParams > systemProperties> systemEnvironment > random > 自定义配置文件