首先Spring Cloud 是基于 Spring 来扩展的,Spring 本身就提供当创建一个Bean时可从Environment 中将一些属性值通过@Value的形式注入到业务代码中的能力。那Spring Cloud Config 要解决的问题就是:
要解决以上三个问题:Spring Cloud Config 规范中刚好定义了核心的三个接口:
在整个 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 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 只提供了具备外部化配置可动态刷新的能力,并不具备自动感知外部化配置发生变更的能力。比如如果你的配置是基于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的方法主要做了两件事:
这两个操作所对应的代码如下所示:
public synchronized Setrefresh() { Map before = extract( this.context.getEnvironment().getPropertySources()); //1、加载最新的值,并替换Envrioment中旧值 addConfigFilesToEnvironment(); Setkeys = 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
可以看到上面的代码中有 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的初始化操作。
企业级互联网架构Aliware,让您的业务能力云化:https://www.aliyun.com/aliware