两个问题:
- 服务端的是如何加载到了远端的配置?
- 为何在pom依赖“spring-cloud-starter-config”后就可以直接使用远端的配置?
一、前置说明:
本来准备看下SpringCloud中有关分布式配置相关的内容,但是在看源码的过程中发现SpringCloud很多组件的实现都是建立在Spring Application和SpringBoot的基础机制之上的。因此如果想真正搞懂SpringCloud中的原理,需要首先对spring-core和spring-context有一定了解。
这里对一些比较重要的关联内容进行说明,如果已经对SpringCloud上下文和SpringCloud的引导启动过程比较熟悉的话可以直接跳过。
1、SpringCloud context
在SpringCloud的官网中有一段说明:
- A Spring Cloud application operates by creating a “bootstrap” context, which is a parent context for the main application. It is responsible for loading configuration properties from the external sources and for decrypting properties in the local external configuration files. The two contexts share an Environment, which is the source of external properties for any Spring application. By default, bootstrap properties (not bootstrap.properties but properties that are loaded during the bootstrap phase) are added with high precedence, so they cannot be overridden by local configuration.
- The bootstrap context uses a different convention for locating external configuration than the main application context. Instead of application.yml (or .properties), you can use bootstrap.yml, keeping the external configuration for bootstrap and main context nicely separate.
这段说明理解起来非常绕,而网上的相关中文资料都为机翻,下面我尝试解释一下这段意思(英文水平有限,见谅)
- Spring Cloud应用进程通过创建一个bootstrap上下文来进行初始化及后续各种操作,此bootstrap上下文是主应用的父上下文。使用此上下文可以加载外部资源的属性配置,并且可以在本地解密从外部资源获取的属性。bootstrap上下文和主应用的上下文共享一个运行环境,在此Spring进程中的各种应用都从此环境获取外部属性资源。一般情况下,bootstrap上下文加载的属性拥有更高的优先级,且不可被本地配置覆盖。(这里所说的,bootstrap上下文加载的属性不一定指在bootstrap配置的属性,而是指所有在启动引导阶段加载的所有属性)
- bootstrap上下文和主应用上下文使用不同的约定进行外部配置。可以使用bootstrap配置文件区别于aplication配置文件,来保证外部配置对于bootstrap上下文和主程序上下文的隔离。
简单来说就是Spring Cloud会在启动的时候创建一个新的context作为Spring主应用进程的父context,并且此context可以加载外部配置,这也是SpringCloud Config的实现基础。
2、ApplicationContext 的层级结构
ConfigurableApplicationContext是ApplicationContext的子接口,这里面有一个方法叫setParent(), 该方法的作用是设置它的父级ApplicationContext。设置父级ApplicationContext后会合并两个context的Environment
@Override
public void setParent(ApplicationContext parent) {
this.parent = parent;
if (parent != null) {
Environment parentEnvironment = parent.getEnvironment();
if (parentEnvironment instanceof ConfigurableEnvironment) {
getEnvironment().merge((ConfigurableEnvironment) parentEnvironment);
}
}
}
3、SpringCloud bootstrap
bootstrap加载于BootstrapApplicationListener
@Override
public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
ConfigurableEnvironment environment = event.getEnvironment();
if (!environment.getProperty("spring.cloud.bootstrap.enabled", Boolean.class,
true)) {
return;
}
// don't listen to events in a bootstrap context
if (environment.getPropertySources().contains(BOOTSTRAP_PROPERTY_SOURCE_NAME)) {
return;
}
ConfigurableApplicationContext context = null;
String configName = environment
.resolvePlaceholders("${spring.cloud.bootstrap.name:bootstrap}");
for (ApplicationContextInitializer<?> initializer : event.getSpringApplication()
.getInitializers()) {
if (initializer instanceof ParentContextApplicationContextInitializer) {
context = findBootstrapContext(
(ParentContextApplicationContextInitializer) initializer,
configName);
}
}
if (context == null) {
context = bootstrapServiceContext(environment, event.getSpringApplication(),
configName);
}
apply(context, event.getSpringApplication(), environment);
}
从代码中可以看到,BootstrapApplicationListener监听了ApplicationEnvironmentPreparedEvent事件,此事件发生于SpringBoot的启动过程中。
其中的ConfigurableApplicationContext可以理解为我们所说bootstrap上下文,如代码所示由bootstrapServiceContext方法初始化
private ConfigurableApplicationContext bootstrapServiceContext(
ConfigurableEnvironment environment, final SpringApplication application,
String configName) {
StandardEnvironment bootstrapEnvironment = new StandardEnvironment();
MutablePropertySources bootstrapProperties = bootstrapEnvironment
.getPropertySources();
for (PropertySource<?> source : bootstrapProperties) {
bootstrapProperties.remove(source.getName());
}
String configLocation = environment
.resolvePlaceholders("${spring.cloud.bootstrap.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);
}
bootstrapProperties.addFirst(
new MapPropertySource(BOOTSTRAP_PROPERTY_SOURCE_NAME, bootstrapMap));
for (PropertySource<?> source : environment.getPropertySources()) {
if (source instanceof StubPropertySource) {
continue;
}
bootstrapProperties.addLast(source);
}
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
// Use names and ensure unique to protect against duplicates
List<String> names = new ArrayList<>(SpringFactoriesLoader
.loadFactoryNames(BootstrapConfiguration.class, classLoader));
for (String name : StringUtils.commaDelimitedListToStringArray(
environment.getProperty("spring.cloud.bootstrap.sources", ""))) {
names.add(name);
}
// TODO: is it possible or sensible to share a ResourceLoader?
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);
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.
builder.application()
.setListeners(filterListeners(builder.application().getListeners()));
}
List<Class<?>> sources = new ArrayList<>();
for (String name : names) {
Class<?> cls = ClassUtils.resolveClassName(name, null);
try {
cls.getDeclaredAnnotations();
}
catch (Exception e) {
continue;
}
sources.add(cls);
}
AnnotationAwareOrderComparator.sort(sources);
builder.sources(sources.toArray(new Class[sources.size()]));
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.
context.setId("bootstrap");
// Make the bootstrap context a parent of the app context
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)
bootstrapProperties.remove(BOOTSTRAP_PROPERTY_SOURCE_NAME);
mergeDefaultProperties(environment.getPropertySources(), bootstrapProperties);
return context;
}
其中的下面这行代码,就是在META-INF/spring.factoies里找配置的BootstrapConfiguration的进行实例化
List<String> names = new ArrayList<>(SpringFactoriesLoader
.loadFactoryNames(BootstrapConfiguration.class, classLoader));
可以进一步看下loadFactoryNames的源码
public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) {
String factoryClassName = factoryClass.getName();
return loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());
}
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
MultiValueMap<String, String> result = cache.get(classLoader);
if (result != null) {
return result;
}
try {
Enumeration<URL> urls = (classLoader != null ?
classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
result = new LinkedMultiValueMap<>();
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
for (Map.Entry<?, ?> entry : properties.entrySet()) {
List<String> factoryClassNames = Arrays.asList(
StringUtils.commaDelimitedListToStringArray((String) entry.getValue()));
result.addAll((String) entry.getKey(), factoryClassNames);
}
}
cache.put(classLoader, result);
return result;
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" +
FACTORIES_RESOURCE_LOCATION + "]", ex);
}
}
其中的FACTORIES_RESOURCE_LOCATION常量值如下定义
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
bootstrap上下文初始化的时候会扫描所有jar下的META-INF/spring.factories文件,并加载其中的配置类。
回到上面bootstrapServiceContext的代码,在加载完bootstrap context后通过addAncestorInitializer(application, context)方法将bootstrap context设置为了当前ApplicationContext的父context
- 总结一下以上流程:
4、Spring Environment 和 springcloud-config Environment
-
Spring Environment:表示当前application的运行环境
-
SpringCloud-config Environment:属性资源的封装对象,用于传递属性到Spring Environment的DTO
这两个Environment要区分开,在SpringCloud Config中会涉及两个Environment中配置属性的传递
二、ConfigServer
1、启动引导类
spring-cloud-config-server jar包中的META-INF/spring.factories文件内容如下:
# Bootstrap components
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
org.springframework.cloud.config.server.bootstrap.ConfigServerBootstrapConfiguration,\
org.springframework.cloud.config.server.config.EncryptionAutoConfiguration
# Application listeners
org.springframework.context.ApplicationListener=\
org.springframework.cloud.config.server.bootstrap.ConfigServerBootstrapApplicationListener
# Autoconfiguration
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.config.server.config.ConfigServerAutoConfiguration,\
org.springframework.cloud.config.server.config.EncryptionAutoConfiguration
其中,ConfigServerBootstrapConfiguration是加载远程配置的主要启动类,EncryptionAutoConfiguration用于资源解密,此处主要看下ConfigServerBootstrapConfiguration
@Configuration
@ConditionalOnProperty("spring.cloud.config.server.bootstrap")
public class ConfigServerBootstrapConfiguration {
@EnableConfigurationProperties(ConfigServerProperties.class)
@Import({ EnvironmentRepositoryConfiguration.class })
protected static class LocalPropertySourceLocatorConfiguration {
@Autowired
private EnvironmentRepository repository;
@Autowired
private ConfigClientProperties client;
@Autowired
private ConfigServerProperties server;
@Bean
public EnvironmentRepositoryPropertySourceLocator environmentRepositoryPropertySourceLocator() {
return new EnvironmentRepositoryPropertySourceLocator(this.repository, this.client.getName(),
this.client.getProfile(), getDefaultLabel());
}
private String getDefaultLabel() {
if (StringUtils.hasText(this.client.getLabel())) {
return this.client.getLabel();
} else if (StringUtils.hasText(this.server.getDefaultLabel())) {
return this.server.getDefaultLabel();
}
return null;
}
}
}
可以看到,此类导入远程仓库的配置been EnvironmentRepositoryConfiguration。在EnvironmentRepositoryConfiguration中包含了git、svn、jdbc等形式的远程配置
看一下EnvironmentRepository接口的定义
public interface EnvironmentRepository {
Environment findOne(String application, String profile, String label);
}
只有一个findOne接口,通过应用名(application)、profile(环境)、lable(分支)拉取远程配置。
需要注意的是,此处的Environment的类型为org.springframework.cloud.config.environment.Environment,需要区分于上面我们所说的上下文的environment。
在ConfigServer的bootstrap引导启动类ConfigServerBootstrapConfiguration主要就是创建了一个PropertySourceLocator接口的实现类EnvironmentRepositoryPropertySourceLocator
PropertySourceLocator是用于具体定位配置属性的接口,只有一个抽象方法
public interface PropertySourceLocator {
/**
* @param environment the current Environment
* @return a PropertySource or null if there is none
*
* @throws IllegalStateException if there is a fail fast condition
*/
PropertySource<?> locate(Environment environment);
}
locate方法的入参Environment是org.springframework.core.env.Environment类型的,这个入参主要用于configClient端使用,在configServer端其实并没有用到。
在实现类EnvironmentRepositoryPropertySourceLocator中,具体实现如下:
@Override
public org.springframework.core.env.PropertySource<?> locate(Environment environment) {
CompositePropertySource composite = new CompositePropertySource("configService");
for (PropertySource source : repository.findOne(name, profiles, label)
.getPropertySources()) {
@SuppressWarnings("unchecked")
Map<String, Object> map = (Map<String, Object>) source.getSource();
composite.addPropertySource(new MapPropertySource(source.getName(), map));
}
return composite;
}
很明显,这里所做的事情就是通过某个具体的repository实现类(具体由配置决定,可以是git、svn等等)获取全部远程配置然后封装在一个PropertySource集合中返回。CompositePropertySource内部封装了一个PropertySource的Set集合。
到目前为止,SpringCloud-config-Server通过bootstrap的启动类获取了全部远程配置,但是这些配置并没有放入到上下文的环境中,也就无法通过Spring管理的方式获取(如@Value方式),下面会讲解远程配置如何加入到环境中。
2、远程配置和本地配置
我们使用spring-cloud-config-server的主要目的是建立一个分布式的远程配置中心。配置中心不仅可以使用本地的配置,同时也会拉去远程配置自己使用或对外提供。
远程配置的拉取和合并是在SpringCloud的bootstrap过程中进行,配置引导类为PropertySourceBootstrapConfiguration,此配置引导类在spring-cloud-context的配置引导类,具体代码如下:
@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);
}
}
locator.locate(environment) 调用这个方法的locator就是我们上面所说的在config-server启动类中创建的EnvironmentRepositoryPropertySourceLocator,其locate方法也就是返回在config-server引导启动过程中加载的全部外部配置。
可以看到最终外部资源结果source放入到了一个名称为BOOTSTRAP_PROPERTY_SOURCE_NAME(bootstrapProperties)的CompositePropertySource中
CompositePropertySource composite = new CompositePropertySource(
BOOTSTRAP_PROPERTY_SOURCE_NAME);
...
source = locator.locate(environment);
...
composite.addPropertySource(source);
然后又从environment中获取了当前环境中的全部配置
MutablePropertySources propertySources = environment.getPropertySources();
MutablePropertySources和CompositePropertySource类似,MutablePropertySources内部封装了一个PropertySource的list集合。
最后通过insertPropertySources(propertySources, composite)方法进行了配置合并,具体代码如下:
private void insertPropertySources(MutablePropertySources propertySources,
CompositePropertySource composite) {
MutablePropertySources incoming = new MutablePropertySources();
incoming.addFirst(composite);
PropertySourceBootstrapProperties remoteProperties = new PropertySourceBootstrapProperties();
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);
}
}
至此ConfigServer获取的外部资源配置加载到了应用上下文环境中
3、对外提供资源配置
SpringCloud-Server通过API对外提供配置服务,具体可见EnvironmentController类
http请求地址和资源文件映射如下:
- /{application}/{profile}[/{label}]
- /{application}-{profile}.yml
- /{label}/{application}-{profile}.yml
- /{application}-{profile}.properties
- /{label}/{application}-{profile}.properties
其中核心方法是labelled,所有的资源获取方法最终都会调用labelled方法,代码如下:
@RequestMapping("/{name}/{profiles}/{label:.*}")
public Environment labelled(@PathVariable String name, @PathVariable String profiles,
@PathVariable String label) {
if (name != null && name.contains("(_)")) {
// "(_)" is uncommon in a git repo name, but "/" cannot be matched
// by Spring MVC
name = name.replace("(_)", "/");
}
if (label != null && label.contains("(_)")) {
// "(_)" is uncommon in a git branch name, but "/" cannot be matched
// by Spring MVC
label = label.replace("(_)", "/");
}
Environment environment = this.repository.findOne(name, profiles, label);
if(!acceptEmpty && (environment == null || environment.getPropertySources().isEmpty())){
throw new EnvironmentNotFoundException("Profile Not found");
}
return environment;
}
方法逻辑非常简单,就是通过repository的findOne方法获取配置并返回
三、springCloud client
在client的META-INF/spring.factories中配置了启动时需要加载的类
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.config.client.ConfigClientAutoConfiguration
# Bootstrap components
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
org.springframework.cloud.config.client.ConfigServiceBootstrapConfiguration,\
org.springframework.cloud.config.client.DiscoveryClientConfigServiceBootstrapConfiguration
首先看一下
ConfigServiceBootstrapConfiguration
@Configuration
@EnableConfigurationProperties
public class ConfigServiceBootstrapConfiguration {
@Autowired
private ConfigurableEnvironment environment;
@Bean
public ConfigClientProperties configClientProperties() {
ConfigClientProperties client = new ConfigClientProperties(this.environment);
return client;
}
@Bean
@ConditionalOnMissingBean(ConfigServicePropertySourceLocator.class)
@ConditionalOnProperty(value = "spring.cloud.config.enabled", matchIfMissing = true)
public ConfigServicePropertySourceLocator configServicePropertySource(ConfigClientProperties properties) {
ConfigServicePropertySourceLocator locator = new ConfigServicePropertySourceLocator(
properties);
return locator;
}
@ConditionalOnProperty(value = "spring.cloud.config.fail-fast")
@ConditionalOnClass({ Retryable.class, Aspect.class, AopAutoConfiguration.class })
@Configuration
@EnableRetry(proxyTargetClass = true)
@Import(AopAutoConfiguration.class)
@EnableConfigurationProperties(RetryProperties.class)
protected static class RetryConfiguration {
@Bean
@ConditionalOnMissingBean(name = "configServerRetryInterceptor")
public RetryOperationsInterceptor configServerRetryInterceptor(
RetryProperties properties) {
return RetryInterceptorBuilder
.stateless()
.backOffOptions(properties.getInitialInterval(),
properties.getMultiplier(), properties.getMaxInterval())
.maxAttempts(properties.getMaxAttempts()).build();
}
}
}
这个类是配置服务的启动类,可以看到其中加载了一个ConfigServicePropertySourceLocator的bean,同ConfigServer中的EnvironmentRepositoryPropertySourceLocator类似,也是PropertySourceLocator的一个实现类
它的作用就是从远程服务器上拿到我们的配置,放入到spring 容器中的environment 中。其locate(Environment environment)方法实现如下:
@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;
}
在ConfigServer中说过,并且在ConfigServicePropertySourceLocator的代码中也可以看到,locate(Environment environment)方法中的Environment类型为org.springframework.core.env.Environment.
这里一定要区分两个Environment,否则难以搞懂其逻辑。
ConfigClient这里的locate方法的代码主要实现如下内容:
1、读取当前上下文环境中的Environment
2、从Environment中读取远程配置ConfigClientProperties(包含profile,name,label,username,password,uri等)
3、通过远程配置获取Config的Environment并返回包含配置属性集合的CompositePropertySource
4、返回的属性配置集合按照ConfigServer中所讲的步骤加入到上下文环境的Envrionment中
三、总结
Server和Client加载配置的简单总结:
从SpringCloud Config-Server和Config-Client的代码中我们可以看到两者实现的基础都是SpringCloud bootstrap的加载过程,而这个过程又是穿插在application的启动过程中的。这里Config组件借助bootstrap的启动过程进行了外部参数的加载,Server端是加载了远程仓库的配置到Environment中,而Client是加载了Server端的配置到自身的Environment中。其实SpringCloud的引导启动过程是一个非常强大的模式,借助这个启动模式SpringCloud可以在其上为SpringBoot添加多种功能。