Spring Cloud Config 规范

0次阅读

共计 8132 个字符,预计需要花费 21 分钟才能阅读完成。

Spring Cloud Config 规范
首先 Spring Cloud 是基于 Spring 来扩展的,Spring 本身就提供当创建一个 Bean 时可从 Environment 中将一些属性值通过 @Value 的形式注入到业务代码中的能力。那 Spring Cloud Config 要解决的问题就是:

如何将配置加载到 Environment。
配置变更时,如何控制 Bean 是否需要 create, 重新触发一次 Bean 的初始化,才能将 @Value 注解指定的字段从 Environment 中重新注入。
配置变更时,如何控制新的配置会更新到 Environment 中,才能保证配置变更时可注入最新的值。

要解决以上三个问题:Spring Cloud Config 规范中刚好定义了核心的三个接口:

PropertySourceLocator:抽象出这个接口,就是让用户可定制化的将一些配置加载到 Environment。这部分的配置获取遵循了 Spring Cloud Config 的理念,即希望能从外部储存介质中来 loacte。
RefreshScope: Spring Cloud 定义这个注解,是扩展了 Spring 原有的 Scope 类型。用来标识当前这个 Bean 是一个 refresh 类型的 Scope。其主要作用就是可以控制 Bean 的整个生命周期。
ContextRefresher:抽象出这个 Class,是让用户自己按需来刷新上下文(比如当有配置刷新时,希望可以刷新上下文,将最新的配置更新到 Environment,重新创建 Bean 时,就可以从 Environment 中注入最新的配置)。

Spring Cloud Config 原理
Spring Cloud Config 的启动过程
1、如何将配置加载到 Environment:PropertySourceLocator
在整个 Spring Boot 启动的生命周期过程中,有一个阶段是 prepare environment。在这个阶段,会 publish 一个 ApplicationEnvironmentPreparedEvent,通知所有对这个事件感兴趣的 Listener, 提供对 Environment 做更多的定制化的操作。Spring Cloud 定义了一个 BootstrapApplicationListener,在 BootstrapApplicationListener 的处理过程中有一步非常关键的操作如下所示:
private ConfigurableApplicationContext bootstrapServiceContext(
ConfigurableEnvironment environment, final SpringApplication application,
String configName) {
// 省略
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
// Use names and ensure unique to protect against duplicates
List<String> names = new ArrayList<>(SpringFactoriesLoader
.loadFactoryNames(BootstrapConfiguration.class, classLoader));
// 省略
}
这是 Spring 的工厂加载机制,可通过在 META-INF/spring.factories 文件中配置一些程序中预定义的一些扩展点。比如 Spring Cloud 这里的实现,可以看到 BootstrapConfiguration 不是一个具体的接口,而是一个注解。通过这种方式配置的扩展点好处是不局限于某一种接口的实现,而是同一类别的实现。可以查看 spring-cloud-context 包中的 spring.factories 文件关于 BootstrapConfiguration 的配置,有一个比较核心入口的配置就是:
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
org.springframework.cloud.bootstrap.config.PropertySourceBootstrapConfiguration
可以发现 PropertySourceBootstrapConfiguration 实现了 ApplicationContextInitializer 接口,其目的就是在应用程序上下文初始化的时候做一些额外的操作。在 Bootstrap 阶段,会通过 Spring Ioc 的整个生命周期来初始化所有通过 key 为_org.springframework.cloud.bootstrap.BootstrapConfiguration_ 在 spring.factories 中配置的 Bean。Spring Cloud Alibaba Nacos Config 的实现就是通过该 key 来自定义一些在 Bootstrap 阶段需要初始化的一些 Bean。在该模块的 spring.factories 配置文件中可以看到如下配置:
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
org.springframework.cloud.alibaba.nacos.NacosConfigBootstrapConfiguration
在 Bootstrap 阶段初始化的过程中,会获取所有 ApplicationContextInitializer 类型的 Bean,并设置回 SpringApplication 主流程当中。如下 BootstrapApplicationListener 类中的部分代码所示:

private void apply(ConfigurableApplicationContext context,

SpringApplication application, ConfigurableEnvironment environment) {
@SuppressWarnings(“rawtypes”)
// 这里的 context 是一个 bootstrap 级别的 ApplicationContext,这里已经含有了在 bootstrap 阶段所有需要初始化的 Bean。
// 因此可以获取 ApplicationContextInitializer.class 类型的所有实例
List<ApplicationContextInitializer> initializers = getOrderedBeansOfType(context,
ApplicationContextInitializer.class);
// 设置回 SpringApplication 主流程当中
application.addInitializers(initializers
.toArray(new ApplicationContextInitializer[initializers.size()]));

// 省略 …
}
这样一来,就可以通过在 SpringApplication 的主流程中来回调这些 ApplicationContextInitializer 的实例,做一些初始化的操作。如下 SpringApplication 类中的部分代码所示:
private void prepareContext(ConfigurableApplicationContext context,
ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments, Banner printedBanner) {
context.setEnvironment(environment);
postProcessApplicationContext(context);
// 回调在 BootstrapApplicationListener 中设置的 ApplicationContextInitializer 实例
applyInitializers(context);
listeners.contextPrepared(context);
// 省略 …
}

protected void applyInitializers(ConfigurableApplicationContext context) {
for (ApplicationContextInitializer initializer : getInitializers()) {
Class<?> requiredType = GenericTypeResolver.resolveTypeArgument(
initializer.getClass(), ApplicationContextInitializer.class);
Assert.isInstanceOf(requiredType, context, “Unable to call initializer.”);
initializer.initialize(context);
}
}
在 applyInitializers 方法中,会触发 PropertySourceBootstrapConfiguration 中的 initialize 方法。如下所示:

@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;
// 回调所有实现 PropertySourceLocator 接口实例的 locate 方法,
source = locator.locate(environment);
if (source == null) {
continue;
}

composite.addPropertySource(source);
empty = false;
}
if (!empty) {
// 从当前 Enviroment 中获取 propertySources
MutablePropertySources propertySources = environment.getPropertySources();
// 省略 …
// 将 composite 中的 PropertySource 添加到当前应用上下文的 propertySources 中
insertPropertySources(propertySources, composite);
// 省略 …
}
在这个方法中会回调所有实现 PropertySourceLocator 接口实例的 locate 方法,locate 方法返回一个 PropertySource 的实例,统一 add 到 CompositePropertySource 实例中。如果 composite 中有新加的 PropertySource, 最后将 composite 中的 PropertySource 添加到当前应用上下文的 propertySources 中。Spring Cloud Alibaba Nacos Config 在 Bootstrap 阶段通过 Java 配置的方式初始化了一个 NacosPropertySourceLocator 类型的 Bean。从而在 locate 方法中将存放在 Nacos 中的配置信息读取出来,将读取结果存放到 PropertySource 的实例中返回。具体如何从 Nacos 中读取配置信息可参考 NacosPropertySourceLocator 类的实现。
Spring Cloud Config 正是提供了 PropertySourceLocator 接口,来提供应用外部化配置可动态加载的能力。Spring Ioc 容器在初始化 Bean 的时候,如果发现 Bean 的字段上含有 @Value 的注解,就会从 Enviroment 中的 PropertySources 来获取其值,完成属性的注入。
Spring Cloud Config 外部化配置可动态刷新
感知到外部化配置的变更这部分代码的操作是需要用户来完成的。Spring Cloud Config 只提供了具备外部化配置可动态刷新的能力,并不具备自动感知外部化配置发生变更的能力。比如如果你的配置是基于 Mysql 来实现的,那么在代码里面肯定要有能力感知到配置发生变化了,然后再显示的调用 ContextRefresher 的 refresh 方法,从而完成外部化配置的动态刷新(只会刷新使用 RefreshScope 注解的 Bean)。
例如在 Spring Cloud Alibaba Nacos Config 的实现过程中,Nacos 提供了对 dataid 变更的 Listener 回调。在对每个 dataid 注册好了相应的 Listener 之后,如果 Nacos 内部通过长轮询的方式感知到数据的变更,就会回调相应的 Listener, 在 Listener 的实现过程中,就是通过调用 ContextRefresher 的 refresh 方法完成配置的动态刷新。具体可参考 NacosContextRefresher 类的实现。
Sring Cloud Config 的动态配置刷新原理图如下所示:

ContextRefresher 的 refresh 的方法主要做了两件事:

触发 PropertySourceLocator 的 locator 方法,需要加载最新的值,并替换 Environment 中旧值
Bean 中的引用配置值需要重新注入一遍。重新注入的流程是在 Bean 初始化时做的操作,那也就是需要将 refresh scope 中的 Bean 缓存失效,当再次从 refresh scope 中获取这个 Bean 时,发现取不到,就会重新触发一次 Bean 的初始化过程。

这两个操作所对应的代码如下所示:

public synchronized Set refresh() {

Map<String, Object> before = extract(
this.context.getEnvironment().getPropertySources());
//1、加载最新的值,并替换 Envrioment 中旧值
addConfigFilesToEnvironment();
Set<String> keys = changes(before,
extract(this.context.getEnvironment().getPropertySources())).keySet();
this.context.publishEvent(new EnvironmentChangeEvent(context, keys));
//2、将 refresh scope 中的 Bean 缓存失效: 清空
this.scope.refreshAll();
return keys;
}
addConfigFilesToEnvironment 方法中发生替换的代码如下所示:

ConfigurableApplicationContext addConfigFilesToEnvironment() {

ConfigurableApplicationContext capture = null;
try {
// 省略 …
//1、这里会重新触发 PropertySourceLoactor 的 locate 的方法, 获取最新的外部化配置
capture = (SpringApplicationBuilder)builder.run();

MutablePropertySources target = this.context.getEnvironment()
.getPropertySources();
String targetName = null;
for (PropertySource<?> source : environment.getPropertySources()) {
String name = source.getName();
// 省略..

// 只有不是标准的 Source 才可替换
if (!this.standardSources.contains(name)) {
if (target.contains(name)) {
// 开始用新的 PropertySource 替换旧值
target.replace(name, source);
}
//
}
}
}
//
return capture;
}
this.scope.refreshAll() 清空缓存的操作代码如下所示:
@Override
public void destroy() {
List<Throwable> errors = new ArrayList<Throwable>();
// 清空 Refresh Scope 中的缓存
Collection<BeanLifecycleWrapper> wrappers = this.cache.clear();
// 省略 …
}

为了验证每次配置刷新时,Bean 是新创建的,特意写了一个 Demo 验证了下,如下所示:
Acm Properties: beijing-region
// 刷新前
Object Instance is :com.alibaba.demo.normal.ConfigProperties@1be9634
2018-11-01 19:16:32.535 INFO 27254 — [gPullingdefault] startup date [Thu Nov 01 19:16:32 CST 2018]; root of context hierarchy
Acm Properties: qingdao-region
// 刷新后
Object Instance is :com.alibaba.demo.normal.ConfigProperties@2c6965e0
Spring Cloud Config 扩展 Scope 的核心类:RefreshScope
可以看到上面的代码中有 this.scope.refreshAll(),其中的 scope 就是 RefreshScope。是用来存放 scope 类型为 refresh 类型的 Bean(即使用 RefreshScope 注解标识的 Bean),也就是说当一个 Bean 既不是 singleton 也不是 prototype 时,就会从自定义的 Scope 中去获取 (Spring 允许自定义 Scope),然后调用 Scope 的 get 方法来获取一个实例,Spring Cloud 正是扩展了 Scope,从而控制了整个 Bean 的生命周期。当配置需要动态刷新的时候,调用 this.scope.refreshAll() 这个方法,就会将整个 RefreshScope 的缓存清空,完成配置可动态刷新的可能。
更多关于 Scope 的分析请参考 这里
后续
关于 ContextRefresh 和 RefreshScope 的初始化配置是在 RefreshAutoConfiguration 类中完成的。而 RefreshAutoConfiguration 类初始化的入口是在 spring-cloud-context 中的 META-INF/spring.factories 中配置的。从而完成整个和动态刷新相关的 Bean 的初始化操作。

本文作者:中间件小哥阅读原文
本文为云栖社区原创内容,未经允许不得转载。

正文完
 0