问题
- Spring Cloud如何创建两个上下文环境的
- Spring Cloud如何加载bootstrap.yml配置文件的
- Spring Cloud Config是如何获取远程配置的
- Spring Cloud多个环境配置项重复优先级问题
源码解析
Spring Cloud如何创建两个上下文环境的
org.springframework.cloud.bootstrap.BootstrapApplicationListener该类执行了创建Spring Cloud父级上下文的创建。
/**
* A listener that prepares a SpringApplication (e.g. populating its Environment) by
* delegating to {@link ApplicationContextInitializer} beans in a separate bootstrap
* context. The bootstrap context is a SpringApplication created from sources defined in
* spring.factories as {@link BootstrapConfiguration}, and initialized with external
* config taken from "bootstrap.properties" (or yml), instead of the normal
* "application.properties".
*
* @author Dave Syer
*
*/
public class BootstrapApplicationListener
implements ApplicationListener<ApplicationEnvironmentPreparedEvent>, Ordered {
// 此处省略代码逻辑
}
BootstrapApplicationListener实现了ApplicationListener,对ApplicationEnvironmentPreparedEvent事件进行了监听,从名字中可以判断Spring会在Environment准备完成时广播该事件。
从org.springframework.boot.SpringApplication的源码中也能看到spring对该事件的通知行为。
package org.springframework.boot;
public class SpringApplication {
// 此处省略其他代码
private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments) {
// Create and configure the environment
ConfigurableEnvironment environment = getOrCreateEnvironment();
configureEnvironment(environment, applicationArguments.getSourceArgs());
ConfigurationPropertySources.attach(environment);
// 此处通知环境加载完毕
listeners.environmentPrepared(environment);
bindToSpringApplication(environment);
if (!this.isCustomEnvironment) {
environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment,
deduceEnvironmentClass());
}
ConfigurationPropertySources.attach(environment);
return environment;
}
}
这里展示下SpringApplication的执行流程图,方便后面理解。
BootstrapApplicationListener接收到ApplicationEnvironmentPreparedEvent事件后执行了如下代码逻辑。
注意,此时还是子级上下文的处理逻辑。
@Override
public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
ConfigurableEnvironment environment = event.getEnvironment();
// 判断是否开启Spring Cloud,默认开启
if (!environment.getProperty("spring.cloud.bootstrap.enabled", Boolean.class,
true)) {
return;
}
// 如果是BootstrapContext则忽略,BOOTSTRAP_PROPERTY_SOURCE_NAME(bootstrap)标识当前上下文为BootstrapContext
if (environment.getPropertySources().contains(BOOTSTRAP_PROPERTY_SOURCE_NAME)) {
return;
}
ConfigurableApplicationContext context = null;
// 获取配置名,默认bootstrap
String configName = environment
.resolvePlaceholders("${spring.cloud.bootstrap.name:bootstrap}");
// 如果已经加载过BootstrapContext了,这里从ParentContextApplicationContextInitializer反射获取BootstrapContext
for (ApplicationContextInitializer<?> initializer : event.getSpringApplication()
.getInitializers()) {
if (initializer instanceof ParentContextApplicationContextInitializer) {
context = findBootstrapContext(
(ParentContextApplicationContextInitializer) initializer,
configName);
}
}
if (context == null) {
// 如果BootstrapContext未曾初始化,这里进行初始化
context = bootstrapServiceContext(environment, event.getSpringApplication(),
configName);
event.getSpringApplication()
.addListeners(new CloseContextOnFailureApplicationListener(context));
}
// 将BootstrapContext的ApplicationContextInitializer配置给子级上下文的SpringApplication
apply(context, event.getSpringApplication(), environment);
}
这里为了区分父级上下文和子级上下文,统一把父级上下文称为BootstrapContext,子级上下文称为ApplicationContext,当然真实项目中可能会有多级上下文,这里只讨论二级的情况。
- 判断是否开启Spring Cloud,默认开启。
- 因为当前是对ApplicationEnvironmentPreparedEvent事件的监听,BootstrapContext的创建一样会触发该监听器,识别到Environment中包含BOOTSTRAP_PROPERTY_SOURCE_NAME(bootstrap)配置则识别为BootstrapContext的ApplicationEnvironmentPreparedEvent事件,直接忽略。
- 获取配置名,默认bootstrap。
- 如果已经加载过BootstrapContext了,这里从ParentContextApplicationContextInitializer反射获取BootstrapContext。
- 如果BootstrapContext未曾创建,即ParentContextApplicationContextInitializer反射没有获取到BootstrapContext,则通过bootstrapServiceContext方法创建BootstrapContext。
- 将BootstrapContext的ApplicationContextInitializer配置给子级上下文的SpringApplication。
private void apply(ConfigurableApplicationContext context,
SpringApplication application, ConfigurableEnvironment environment) {
@SuppressWarnings("rawtypes")
List<ApplicationContextInitializer> initializers = getOrderedBeansOfType(context,
ApplicationContextInitializer.class);
application.addInitializers(initializers
.toArray(new ApplicationContextInitializer[initializers.size()]));
addBootstrapDecryptInitializer(application);
}
context = bootstrapServiceContext(environment, event.getSpringApplication(), configName); 这段代码完成了对BootstrapContext的创建,让我们来看下该代码的具体逻辑。
private ConfigurableApplicationContext bootstrapServiceContext(
ConfigurableEnvironment environment, final SpringApplication application,
String configName) {
// 创建BootstrapContext的Environment
StandardEnvironment bootstrapEnvironment = new StandardEnvironment();
MutablePropertySources bootstrapProperties = bootstrapEnvironment
.getPropertySources();
for (PropertySource<?> source : bootstrapProperties) {
bootstrapProperties.remove(source.getName());
}
// 设置BootstrapContext初始化配置信息
String configLocation = environment
.resolvePlaceholders("${spring.cloud.bootstrap.location:}");
String configAdditionalLocation = environment
.resolvePlaceholders("${spring.cloud.bootstrap.additional-location:}");
Map<String, Object> bootstrapMap = new HashMap<>();
bootstrapMap.put("spring.config.name", configName);
// if an app (or test) uses spring.main.web-application-type=reactive, bootstrap
// will fail
// force the environment to use none, because if though it is set below in the
// builder
// the environment overrides it
bootstrapMap.put("spring.main.web-application-type", "none");
if (StringUtils.hasText(configLocation)) {
bootstrapMap.put("spring.config.location", configLocation);
}
if (StringUtils.hasText(configAdditionalLocation)) {
bootstrapMap.put("spring.config.additional-location",
configAdditionalLocation);
}
bootstrapProperties.addFirst(
new MapPropertySource(BOOTSTRAP_PROPERTY_SOURCE_NAME, bootstrapMap));
for (PropertySource<?> source : environment.getPropertySources()) {
if (source instanceof StubPropertySource) {
continue;
}
bootstrapProperties.addLast(source);
}
// TODO: is it possible or sensible to share a ResourceLoader?
// 创建BootstrapContext对应的SpringApplication
SpringApplicationBuilder builder = new SpringApplicationBuilder()
.profiles(environment.getActiveProfiles()).bannerMode(Mode.OFF)
.environment(bootstrapEnvironment)
// Don't use the default properties in this builder
.registerShutdownHook(false).logStartupInfo(false)
.web(WebApplicationType.NONE);
final SpringApplication builderApplication = builder.application();
if (builderApplication.getMainApplicationClass() == null) {
// gh_425:
// SpringApplication cannot deduce the MainApplicationClass here
// if it is booted from SpringBootServletInitializer due to the
// absense of the "main" method in stackTraces.
// But luckily this method's second parameter "application" here
// carries the real MainApplicationClass which has been explicitly
// set by SpringBootServletInitializer itself already.
builder.main(application.getMainApplicationClass());
}
if (environment.getPropertySources().contains("refreshArgs")) {
// If we are doing a context refresh, really we only want to refresh the
// Environment, and there are some toxic listeners (like the
// LoggingApplicationListener) that affect global static state, so we need a
// way to switch those off.
builderApplication
.setListeners(filterListeners(builderApplication.getListeners()));
}
// 设置Spring Cloud的配置类,使得Spring Cloud新特性可以生效
builder.sources(BootstrapImportSelectorConfiguration.class);
// 运行BootstrapContext对应的SpringApplication
final ConfigurableApplicationContext context = builder.run();
// gh-214 using spring.application.name=bootstrap to set the context id via
// `ContextIdApplicationContextInitializer` prevents apps from getting the actual
// spring.application.name
// during the bootstrap phase.
// 设置BootstrapContext的ID
context.setId("bootstrap");
// Make the bootstrap context a parent of the app context
// 设置AncestorInitializer
addAncestorInitializer(application, context);
// It only has properties in it now that we don't want in the parent so remove
// it (and it will be added back later)
// 移除BOOTSTRAP_PROPERTY_SOURCE_NAME(bootstrap)配置
bootstrapProperties.remove(BOOTSTRAP_PROPERTY_SOURCE_NAME(bootstrap)配置);
mergeDefaultProperties(environment.getPropertySources(), bootstrapProperties);
return context;
}
- 创建BootstrapContext的Environment。
- 设置BootstrapContext初始化配置信息,包括以下配置:
配置项 | 配置名 | 默认值 |
spring.main.web-application-type | 应用类型 | none |
spring.config.location | 配置文件地址,可通过spring.cloud.bootstrap.location配置 | |
spring.config.additional-location | 额外配置文件地址,可通过spring.cloud.bootstrap.additional-location配置 | |
spring.config.name | 环境配置名称,可通过spring.cloud.bootstrap.name配置 | bootstrap |
- 其中比较重要的是spring.config.name配置项,SpringBoot通过该配置项来获取bootstrap.yml或者bootstrap.properties配置文件(默认bootstrap情况下)。具体如何获取的稍后介绍。
- 创建BootstrapContext对应的SpringApplication。
- 设置Spring Cloud的配置类,使得Spring Cloud新特性可以生效。具体做了哪些配置后面介绍。
- 运行BootstrapContext对应的SpringApplication,获取到ConfigurableApplicationContext(即BootstrapContext)。
- 设置BootstrapContext的ID,用于区分ApplicatonContext。
- 给子级ApplicationContext设置AncestorInitializer。这个AncestorInitializer实现了ApplicationContextInitializer,ApplicationContextInitializer会在SpringApplication创建ApplicationContext后执行,可参考上面SpringApplication的执行流程图。
会将当前BootstrapContext作为构造方法的参数传入AncestorInitializer,等子级对应的SpringApplication创建ApplicationContext后会执行该AncestorInitializer。 - 移除BOOTSTRAP_PROPERTY_SOURCE_NAME(bootstrap)配置(BootstrapContext对应的Environment)。
private void addAncestorInitializer(SpringApplication application,
ConfigurableApplicationContext context) {
boolean installed = false;
for (ApplicationContextInitializer<?> initializer : application
.getInitializers()) {
if (initializer instanceof AncestorInitializer) {
installed = true;
// New parent
// 如果子级已经有该AncestorInitializer了,直接将BootstrapContext设置给该AncestorInitializer
((AncestorInitializer) initializer).setParent(context);
}
}
if (!installed) {
// 设置AncestorInitializer到子级SpringApplication
application.addInitializers(new AncestorInitializer(context));
}
}
private static class AncestorInitializer implements
ApplicationContextInitializer<ConfigurableApplicationContext>, Ordered {
private ConfigurableApplicationContext parent;
AncestorInitializer(ConfigurableApplicationContext parent) {
this.parent = parent;
}
@Override
public void initialize(ConfigurableApplicationContext context) {
// 递归查询顶级ApplicationContext
while (context.getParent() != null && context.getParent() != context) {
context = (ConfigurableApplicationContext) context.getParent();
}
reorderSources(context.getEnvironment());
// 通过ParentContextApplicationContextInitializer类将Bootstrap设置为顶级上下文
new ParentContextApplicationContextInitializer(this.parent)
.initialize(context);
}
}
public class ParentContextApplicationContextInitializer
implements ApplicationContextInitializer<ConfigurableApplicationContext>, Ordered {
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
if (applicationContext != this.parent) {
// 将Bootstrap设置为顶级上下文
applicationContext.setParent(this.parent);
// 设置一个监听器,通知ParentContextAvailableEvent事件
applicationContext.addApplicationListener(EventPublisher.INSTANCE);
}
}
}
Spring Cloud如何加载bootstrap.yml配置文件的
org.springframework.boot.context.config.ConfigFileApplicationListener会加载本地配置到当前上下文环境中,默认情况下会加载application.yml/application.properties
package org.springframework.boot.context.config;
public class ConfigFileApplicationListener implements EnvironmentPostProcessor, SmartApplicationListener, Ordered {
int Order.HIGHEST_PRECEDENCE = Integer.MIN_VALUE;
public static final int DEFAULT_ORDER = Ordered.HIGHEST_PRECEDENCE + 10;
private int order = DEFAULT_ORDER;
@Override
public void onApplicationEvent(ApplicationEvent event) {
// 监听ApplicationEnvironmentPreparedEvent事件
if (event instanceof ApplicationEnvironmentPreparedEvent) {
onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent) event);
}
// 监听ApplicationPreparedEvent事件
if (event instanceof ApplicationPreparedEvent) {
onApplicationPreparedEvent(event);
}
}
// 对ApplicationEnvironmentPreparedEvent事件的逻辑处理
private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
// 装载EnvironmentPostProcessor
List<EnvironmentPostProcessor> postProcessors = loadPostProcessors();
// 因为自身也实现了EnvironmentPostProcessor,所以把自身也装载进去
postProcessors.add(this);
// 排序
AnnotationAwareOrderComparator.sort(postProcessors);
for (EnvironmentPostProcessor postProcessor : postProcessors) {
// 循环处理EnvironmentPostProcessor的逻辑
postProcessor.postProcessEnvironment(event.getEnvironment(), event.getSpringApplication());
}
}
// 通过spring.factoies加载EnvironmentPostProcessor
List<EnvironmentPostProcessor> loadPostProcessors() {
return SpringFactoriesLoader.loadFactories(EnvironmentPostProcessor.class, getClass().getClassLoader());
}
// 获取执行顺序排序值,值越小越先执行,
@Override
public int getOrder() {
return this.order;
}
}
可以看到ConfigFileApplicationListener也继承了ApplicationListener(通过SmartApplicationListener间接继承)接口,并且监听了ApplicationEnvironmentPreparedEvent事件。
因为执行顺序排寻值为Ordered.HIGHEST_PRECEDENCE + 10,而上面介绍的org.springframework.cloud.bootstrap.BootstrapApplicationListener(创建BootstrapContext的监听器,同样监听了ApplicationEnvironmentPreparedEvent事件),执行顺序排序值为Ordered.HIGHEST_PRECEDENCE + 5, 比Ordered.HIGHEST_PRECEDENCE + 10要小,所以BootstrapApplicationListener要比ConfigFileApplicationListener先执行。
通过上面的代码以及注释我们可以看到,ConfigFileApplicationListener会作为一个EnvironmentPostProcessor的实现类,当监听到ApplicationEnvironmentPreparedEvent时,会通过postProcessEnvironment对Environment进行配置。
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
addPropertySources(environment, application.getResourceLoader());
}
protected void addPropertySources(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
RandomValuePropertySource.addToEnvironment(environment);
new Loader(environment, resourceLoader).load();
}
private class Loader {
private final ConfigurableEnvironment environment;
private final ResourceLoader resourceLoader;
private Map<DocumentsCacheKey, List<Document>> loadDocumentsCache = new HashMap<>();
Loader(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
this.environment = environment;
this.placeholdersResolver = new PropertySourcesPlaceholdersResolver(this.environment);
this.resourceLoader = (resourceLoader != null) ? resourceLoader : new DefaultResourceLoader(null);
this.propertySourceLoaders = SpringFactoriesLoader.loadFactories(PropertySourceLoader.class,
getClass().getClassLoader());
}
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);
});
}
}
这里代码逻辑就不深入了,大概逻辑就是会通过查找本地的配置信息并装载到Environment, 默认的就是application.properties/application.yml,当然也会根据激活的profile信息查找对应的配置文件,如profile=dev情况下会查找application-dev.properties/application-dev.yml,有兴趣的同学可以自行研究。
这里我们需要关心的是获取配置文件名的代码逻辑。
public static final String CONFIG_NAME_PROPERTY = "spring.config.name";
private static final String DEFAULT_NAMES = "application";
private Set<String> getSearchNames() {
if (this.environment.containsProperty(CONFIG_NAME_PROPERTY)) {
// 如果存在spring.config.name配置的值,则获取该值作为配置文件名
String property = this.environment.getProperty(CONFIG_NAME_PROPERTY);
Set<String> names = asResolvedSet(property, null);
names.forEach(this::assertValidConfigName);
return names;
}
return asResolvedSet(ConfigFileApplicationListener.this.names, DEFAULT_NAMES);
}
该代码的逻辑大概就是如果存在spring.config.name配置的值,则获取该值作为配置文件名。
而通过之前的梳理我们已经知道了,BootstrapApplicationListener在创建BootstrapContext后会将spring.config.name设置为bootstrap(默认),所以这里ConfigFileApplicationListener会通过这个文件名bootstrap.yml/bootstrap.properties来查找配置,并加载到Environment中。当然这里配置比较复杂,还有默认路径配置、文件类型加载等,这里就不展开了。
Spring Cloud Config是如何获取远程配置的
org.springframework.cloud.bootstrap.config.PropertySourceBootstrapConfiguration为Spring Cloud加载远程配置。
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(PropertySourceBootstrapProperties.class)
public class PropertySourceBootstrapConfiguration implements
ApplicationContextInitializer<ConfigurableApplicationContext>, Ordered {
// 此处省略代码逻辑
}
可以看到这里PropertySourceBootstrapConfiguration也实现了ApplicationContextInitializer,即在ApplicationContext生成后机会执行initialize方法。但这个类是如何被加载到Spring容器中的呢?
// BootstrapApplicationListener#bootstrapServiceContext里为BootstrapContexat设置了一个配置源
builder.sources(BootstrapImportSelectorConfiguration.class);
BootstrapApplicationListener#bootstrapServiceContext里为BootstrapContext设置了一个配置源。
@Configuration(proxyBeanMethods = false)
@Import(BootstrapImportSelector.class)
public class BootstrapImportSelectorConfiguration {
}
该配置导入了另外一个配置org.springframework.cloud.bootstrap.BootstrapImportSelector。
public class BootstrapImportSelector implements EnvironmentAware, DeferredImportSelector {
private Environment environment;
private MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory();
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
// Use names and ensure unique to protect against duplicates
List<String> names = new ArrayList<>(SpringFactoriesLoader
.loadFactoryNames(BootstrapConfiguration.class, classLoader));
names.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(
this.environment.getProperty("spring.cloud.bootstrap.sources", ""))));
List<OrderedAnnotatedElement> elements = new ArrayList<>();
for (String name : names) {
try {
elements.add(
new OrderedAnnotatedElement(this.metadataReaderFactory, name));
}
catch (IOException e) {
continue;
}
}
AnnotationAwareOrderComparator.sort(elements);
String[] classNames = elements.stream().map(e -> e.name).toArray(String[]::new);
return classNames;
}
这段代码大致逻辑就是将spring.facories配置文件中BootstrapConfiguration.class对应的实现类(此处实现类非JAVA意义上的实现类)加载为Spring配置类。
最后我们可以在spring.facories配置文件中看到PropertySourceBootstrapConfiguration。
在回到BootstrapApplicationListener#onApplicationEvent方法中,当BootstrapContext创建后会调用org.springframework.cloud.bootstrap.BootstrapApplicationListener#apply方法,这时会从父容器也就是BootstrapContext中获取ApplicationContextInitializer接口对应的Bean,并将其设置为子容器的ApplicationContextInitializer,当子容器生成时就会执行这些定制器。
最后来看下PropertySourceBootstrapConfiguration里是如何加载远程配置的。
@Autowired(required = false)
private List<PropertySourceLocator> propertySourceLocators = new ArrayList<>();
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
CompositePropertySource composite = new CompositePropertySource(
BOOTSTRAP_PROPERTY_SOURCE_NAME);
AnnotationAwareOrderComparator.sort(this.propertySourceLocators);
boolean empty = true;
ConfigurableEnvironment environment = applicationContext.getEnvironment();
for (PropertySourceLocator locator : this.propertySourceLocators) {
PropertySource<?> source = null;
source = locator.locate(environment);
if (source == null) {
continue;
}
logger.info("Located property source: " + source);
composite.addPropertySource(source);
empty = false;
}
if (!empty) {
MutablePropertySources propertySources = environment.getPropertySources();
String logConfig = environment.resolvePlaceholders("${logging.config:}");
LogFile logFile = LogFile.get(environment);
if (propertySources.contains(BOOTSTRAP_PROPERTY_SOURCE_NAME)) {
propertySources.remove(BOOTSTRAP_PROPERTY_SOURCE_NAME);
}
insertPropertySources(propertySources, composite);
reinitializeLoggingSystem(environment, logConfig, logFile);
setLogLevels(applicationContext, environment);
handleIncludedProfiles(environment);
}
}
这里可以看到PropertySourceBootstrapConfiguration通过循环propertySourceLocators列表来获取配置信息。
而propertySourceLocators是PropertySourceLocator接口实现类的列表,通过@Autowired注解注入。我们可以自己实现PropertySourceLocator接口,并注册到Spring父容器中(BootstrapContext)。通过在spring.factories中添加如下配置即可添加Bean到父容器。
# 注册DemoPropertySourceLocator到spring父容器
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
test.customized.bean.DemoPropertySourceLocator
/*
* Copyright 2013-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.config.client;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.bootstrap.config.PropertySourceLocator;
import org.springframework.cloud.config.client.ConfigClientProperties.Credentials;
import org.springframework.cloud.config.environment.Environment;
import org.springframework.cloud.config.environment.PropertySource;
import org.springframework.core.annotation.Order;
import org.springframework.core.env.CompositePropertySource;
import org.springframework.core.env.MapPropertySource;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.retry.annotation.Retryable;
import org.springframework.util.Base64Utils;
import org.springframework.util.StringUtils;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.HttpServerErrorException;
import org.springframework.web.client.ResourceAccessException;
import org.springframework.web.client.RestTemplate;
import static org.springframework.cloud.config.client.ConfigClientProperties.STATE_HEADER;
import static org.springframework.cloud.config.client.ConfigClientProperties.TOKEN_HEADER;
import static org.springframework.cloud.config.client.ConfigClientProperties.AUTHORIZATION;
/**
* @author Dave Syer
* @author Mathieu Ouellet
*
*/
@Order(0)
public class ConfigServicePropertySourceLocator implements PropertySourceLocator {
private static Log logger = LogFactory
.getLog(ConfigServicePropertySourceLocator.class);
private RestTemplate restTemplate;
private ConfigClientProperties defaultProperties;
public ConfigServicePropertySourceLocator(ConfigClientProperties defaultProperties) {
this.defaultProperties = defaultProperties;
}
@Override
@Retryable(interceptor = "configServerRetryInterceptor")
public org.springframework.core.env.PropertySource<?> locate(
org.springframework.core.env.Environment environment) {
ConfigClientProperties properties = this.defaultProperties.override(environment);
CompositePropertySource composite = new CompositePropertySource("configService");
RestTemplate restTemplate = this.restTemplate == null
? getSecureRestTemplate(properties)
: this.restTemplate;
Exception error = null;
String errorBody = null;
try {
String[] labels = new String[] { "" };
if (StringUtils.hasText(properties.getLabel())) {
labels = StringUtils
.commaDelimitedListToStringArray(properties.getLabel());
}
String state = ConfigClientStateHolder.getState();
// Try all the labels until one works
for (String label : labels) {
Environment result = getRemoteEnvironment(restTemplate, properties,
label.trim(), state);
if (result != null) {
log(result);
if (result.getPropertySources() != null) { // result.getPropertySources()
// can be null if using
// xml
for (PropertySource source : result.getPropertySources()) {
@SuppressWarnings("unchecked")
Map<String, Object> map = (Map<String, Object>) source
.getSource();
composite.addPropertySource(
new MapPropertySource(source.getName(), map));
}
}
if (StringUtils.hasText(result.getState())
|| StringUtils.hasText(result.getVersion())) {
HashMap<String, Object> map = new HashMap<>();
putValue(map, "config.client.state", result.getState());
putValue(map, "config.client.version", result.getVersion());
composite.addFirstPropertySource(
new MapPropertySource("configClient", map));
}
return composite;
}
}
}
catch (HttpServerErrorException e) {
error = e;
if (MediaType.APPLICATION_JSON
.includes(e.getResponseHeaders().getContentType())) {
errorBody = e.getResponseBodyAsString();
}
}
catch (Exception e) {
error = e;
}
if (properties.isFailFast()) {
throw new IllegalStateException(
"Could not locate PropertySource and the fail fast property is set, failing",
error);
}
logger.warn("Could not locate PropertySource: " + (errorBody == null
? error == null ? "label not found" : error.getMessage()
: errorBody));
return null;
}
private void log(Environment result) {
if (logger.isInfoEnabled()) {
logger.info(String.format(
"Located environment: name=%s, profiles=%s, label=%s, version=%s, state=%s",
result.getName(),
result.getProfiles() == null ? ""
: Arrays.asList(result.getProfiles()),
result.getLabel(), result.getVersion(), result.getState()));
}
if (logger.isDebugEnabled()) {
List<PropertySource> propertySourceList = result.getPropertySources();
if (propertySourceList != null) {
int propertyCount = 0;
for (PropertySource propertySource : propertySourceList) {
propertyCount += propertySource.getSource().size();
}
logger.debug(String.format(
"Environment %s has %d property sources with %d properties.",
result.getName(), result.getPropertySources().size(),
propertyCount));
}
}
}
private void putValue(HashMap<String, Object> map, String key, String value) {
if (StringUtils.hasText(value)) {
map.put(key, value);
}
}
private Environment getRemoteEnvironment(RestTemplate restTemplate,
ConfigClientProperties properties, String label, String state) {
String path = "/{name}/{profile}";
String name = properties.getName();
String profile = properties.getProfile();
String token = properties.getToken();
int noOfUrls = properties.getUri().length;
if (noOfUrls > 1) {
logger.info("Multiple Config Server Urls found listed.");
}
Object[] args = new String[] { name, profile };
if (StringUtils.hasText(label)) {
if (label.contains("/")) {
label = label.replace("/", "(_)");
}
args = new String[] { name, profile, label };
path = path + "/{label}";
}
ResponseEntity<Environment> response = null;
for (int i = 0; i < noOfUrls; i++) {
Credentials credentials = properties.getCredentials(i);
String uri = credentials.getUri();
String username = credentials.getUsername();
String password = credentials.getPassword();
logger.info("Fetching config from server at : " + uri);
try {
HttpHeaders headers = new HttpHeaders();
addAuthorizationToken(properties, headers, username, password);
if (StringUtils.hasText(token)) {
headers.add(TOKEN_HEADER, token);
}
if (StringUtils.hasText(state) && properties.isSendState()) {
headers.add(STATE_HEADER, state);
}
final HttpEntity<Void> entity = new HttpEntity<>((Void) null, headers);
response = restTemplate.exchange(uri + path, HttpMethod.GET, entity,
Environment.class, args);
}
catch (HttpClientErrorException e) {
if (e.getStatusCode() != HttpStatus.NOT_FOUND) {
throw e;
}
}
catch (ResourceAccessException e) {
logger.info("Connect Timeout Exception on Url - " + uri
+ ". Will be trying the next url if available");
if (i == noOfUrls - 1)
throw e;
else
continue;
}
if (response == null || response.getStatusCode() != HttpStatus.OK) {
return null;
}
Environment result = response.getBody();
return result;
}
return null;
}
public void setRestTemplate(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
private RestTemplate getSecureRestTemplate(ConfigClientProperties client) {
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
if (client.getRequestReadTimeout() < 0) {
throw new IllegalStateException("Invalid Value for Read Timeout set.");
}
requestFactory.setReadTimeout(client.getRequestReadTimeout());
RestTemplate template = new RestTemplate(requestFactory);
Map<String, String> headers = new HashMap<>(client.getHeaders());
if (headers.containsKey(AUTHORIZATION)) {
headers.remove(AUTHORIZATION); // To avoid redundant addition of header
}
if (!headers.isEmpty()) {
template.setInterceptors(Arrays.<ClientHttpRequestInterceptor> asList(
new GenericRequestHeaderInterceptor(headers)));
}
return template;
}
private void addAuthorizationToken(ConfigClientProperties configClientProperties,
HttpHeaders httpHeaders, String username, String password) {
String authorization = configClientProperties.getHeaders().get(AUTHORIZATION);
if (password != null && authorization != null) {
throw new IllegalStateException(
"You must set either 'password' or 'authorization'");
}
if (password != null) {
byte[] token = Base64Utils.encode((username + ":" + password).getBytes());
httpHeaders.add("Authorization", "Basic " + new String(token));
}
else if (authorization != null) {
httpHeaders.add("Authorization", authorization);
}
}
public static class GenericRequestHeaderInterceptor
implements ClientHttpRequestInterceptor {
private final Map<String, String> headers;
public GenericRequestHeaderInterceptor(Map<String, String> headers) {
this.headers = headers;
}
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body,
ClientHttpRequestExecution execution) throws IOException {
for (Entry<String, String> header : headers.entrySet()) {
request.getHeaders().add(header.getKey(), header.getValue());
}
return execution.execute(request, body);
}
protected Map<String, String> getHeaders() {
return headers;
}
}
}
这里贴一下Spring Cloud Config的org.springframework.cloud.config.client.ConfigServicePropertySourceLocator是加载远程配置的代码。
Spring Cloud多个环境配置项重复优先级问题
public abstract class AbstractApplicationContext extends DefaultResourceLoader
implements ConfigurableApplicationContext {
/**
* Set the parent of this application context.
* <p>The parent {@linkplain ApplicationContext#getEnvironment() environment} is
* {@linkplain ConfigurableEnvironment#merge(ConfigurableEnvironment) merged} with
* this (child) application context environment if the parent is non-{@code null} and
* its environment is an instance of {@link ConfigurableEnvironment}.
* @see ConfigurableEnvironment#merge(ConfigurableEnvironment)
*/
@Override
public void setParent(@Nullable ApplicationContext parent) {
this.parent = parent;
if (parent != null) {
Environment parentEnvironment = parent.getEnvironment();
if (parentEnvironment instanceof ConfigurableEnvironment) {
// 合并父级上下文环境
getEnvironment().merge((ConfigurableEnvironment) parentEnvironment);
}
}
}
}
SpringApplication设置父级上线文时候会进行Environment的合并,本文中即合并父级上下文BootstrapContext对应的Environment。
@Override
public void merge(ConfigurableEnvironment parent) {
for (PropertySource<?> ps : parent.getPropertySources()) {
if (!this.propertySources.contains(ps.getName())) {
this.propertySources.addLast(ps);
}
}
String[] parentActiveProfiles = parent.getActiveProfiles();
if (!ObjectUtils.isEmpty(parentActiveProfiles)) {
synchronized (this.activeProfiles) {
for (String profile : parentActiveProfiles) {
this.activeProfiles.add(profile);
}
}
}
String[] parentDefaultProfiles = parent.getDefaultProfiles();
if (!ObjectUtils.isEmpty(parentDefaultProfiles)) {
synchronized (this.defaultProfiles) {
this.defaultProfiles.remove(RESERVED_DEFAULT_PROFILE_NAME);
for (String profile : parentDefaultProfiles) {
this.defaultProfiles.add(profile);
}
}
}
}
子级中没有配置名对应的配置项才会保留到子级上下文环境中,此处的配置名并非配置项名,而是某个配置源的配置名,如通过PropertySourceBootstrapConfiguration加载的配置项的配置源名为bootstrapProperties。
PropertySourceBootstrapConfiguration是在上下文合并后,子级上下文创建后执行的,比ConfigFileApplicationListener执行要晚,所以此时已经有application.properties/application.yml的配置信息在子级Environment中了。
PropertySourceBootstrapConfiguration#insertPropertySources方法定义了远程配置的顺序逻辑。
private void insertPropertySources(MutablePropertySources propertySources,
CompositePropertySource composite) {
MutablePropertySources incoming = new MutablePropertySources();
incoming.addFirst(composite);
PropertySourceBootstrapProperties remoteProperties = new PropertySourceBootstrapProperties();
// 绑定配置信息到remoteProperties
Binder.get(environment(incoming)).bind("spring.cloud.config", Bindable.ofInstance(remoteProperties));
if (!remoteProperties.isAllowOverride() || (!remoteProperties.isOverrideNone()
&& remoteProperties.isOverrideSystemProperties())) {
// 如果不允许额外配置被覆盖配置或者 允许覆盖额外配置但覆盖级别为高优先级并且允许覆盖系统配置
// 则将额外配置放在第一位
propertySources.addFirst(composite);
return;
}
if (remoteProperties.isOverrideNone()) {
// 如果额外配置为低优先级,则将额外配置放末尾
propertySources.addLast(composite);
return;
}
if (propertySources
.contains(StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME)) {
if (!remoteProperties.isOverrideSystemProperties()) {
// 原先已经包含系统配置,并且不允许覆盖系统配置,则将额外配置放在系统配置之后
propertySources.addAfter(
StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME,
composite);
}
else {
// 否则放在系统配置之前
propertySources.addBefore(
StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME,
composite);
}
}
else {
// 没有系统配置则放在配置末尾
propertySources.addLast(composite);
}
}
@ConfigurationProperties("spring.cloud.config")
public class PropertySourceBootstrapProperties {
/**
* Flag to indicate that the external properties should override system properties.
* Default true.
*/
private boolean overrideSystemProperties = true;
/**
* Flag to indicate that {@link #isOverrideSystemProperties()
* systemPropertiesOverride} can be used. Set to false to prevent users from changing
* the default accidentally. Default true.
*/
private boolean allowOverride = true;
/**
* Flag to indicate that when {@link #setAllowOverride(boolean) allowOverride} is
* true, external properties should take lowest priority, and not override any
* existing property sources (including local config files). Default false.
*/
private boolean overrideNone = false;
}
根据源码可以看到PropertySourceBootstrapConfiguration根据PropertySourceBootstrapProperties的配置信息来决定额外配置(远程配置)的优先级。
配置项 | 配置信息 | 默认值 |
overrideSystemProperties | 允许额外配置覆盖系统配置 | true |
allowOverride | 额外配置是否允许被覆盖 | true |
overrideNone | 额外配置是否为低优先级 | false |
默认情况下额外配置不允许被覆盖,并且为高优先级,并且覆盖系统配置。
public class PropertySourcesPropertyResolver extends AbstractPropertyResolver {
@Nullable
protected <T> T getProperty(String key, Class<T> targetValueType, boolean resolveNestedPlaceholders) {
if (this.propertySources != null) {
for (PropertySource<?> propertySource : this.propertySources) {
if (logger.isTraceEnabled()) {
logger.trace("Searching for key '" + key + "' in PropertySource '" +
propertySource.getName() + "'");
}
Object value = propertySource.getProperty(key);
if (value != null) {
if (resolveNestedPlaceholders && value instanceof String) {
value = resolveNestedPlaceholders((String) value);
}
logKeyFound(key, propertySource, value);
return convertValueIfNecessary(value, targetValueType);
}
}
}
if (logger.isDebugEnabled()) {
logger.debug("Could not find key '" + key + "' in any property source");
}
return null;
}
}
Environment委托PropertySourcesPropertyResolver来获取环境变量值,而上面代码中可以看到PropertySourcesPropertyResolver通过循环propertySources列表来获取具体的PropertySource,并通过key从每个PropertySource中获取具体的属性值,如果获取到具体值则转换后返回。所以排在前面的PropertySource获取配置值的优先级更高。