首先Spring Cloud 是基於 Spring 來擴展的,Spring 自己就提供當建立一個Bean時可從Environment 中將一些屬性值經過@Value的形式注入到業務代碼中的能力。那Spring Cloud Config 要解決的問題就是:html
要解決以上三個問題:Spring Cloud Config 規範中恰好定義了核心的三個接口:spring
在整個 Spring Boot 啓動的生命週期過程當中,有一個階段是 prepare environment。在這個階段,會publish 一個 ApplicationEnvironmentPreparedEvent,通知全部對這個事件感興趣的 Listener,提供對 Environment 作更多的定製化的操做。Spring Cloud 定義了一個BootstrapApplicationListener,在 BootstrapApplicationListener 的處理過程當中有一步很是關鍵的操做以下所示:sql
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的配置,有一個比較核心入口的配置就是:bootstrap
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 類中的部分代碼所示:app
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 類中的部分代碼所示:ide
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 方法。以下所示:post
@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 類的實現。ui
Spring Cloud Config 正是提供了PropertySourceLocator接口,來提供應用外部化配置可動態加載的能力。Spring Ioc 容器在初始化 Bean 的時候,若是發現 Bean 的字段上含有 @Value 的註解,就會從 Enviroment 中的PropertySources 來獲取其值,完成屬性的注入。this
感知到外部化配置的變動這部分代碼的操做是須要用戶來完成的。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 Set refresh() {
Map<String, Object> before = extract( this.context.getEnvironment().getPropertySources()); //一、加載最新的值,並替換Envrioment中舊值 addConfigFilesToEnvironment(); Set<String> keys = changes(before, extract(this.context.getEnvironment().getPropertySources())).keySet(); this.context.publishEvent(new EnvironmentChangeEvent(context, keys)); //二、將refresh scope中的Bean 緩存失效: 清空 this.scope.refreshAll(); return keys; }
addConfigFilesToEnvironment 方法中發生替換的代碼以下所示:
ConfigurableApplicationContext addConfigFilesToEnvironment() {
ConfigurableApplicationContext capture = null; try { //省略... //一、這裏會從新觸發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的初始化操做。