SpringBoot源码系列:Environment机制深入分析(二)

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模块

当启动程序加载以上三个配置文件的时候 会执行以下的顺序:

  1. 加载application.yml文件的主模块的内容
  2. 将application.yml中的spring.profiles.active 放入待解析的集合中
  3. 加载application-development.yml文件并解析主模块内容
  4. 解析application-development.yml文件的development模块内容
  5. 解析application.yml中development模块的内容
  6. 加载application-test.yml文件 并解析主模块内容
  7. 解析application-test.yml文件中的test模块内容
  8. 解析application-development.yml文件中test模块中的内容
  9. 解析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 > 自定义配置文件

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值