Spring Cloud Config 規範

java那些事2018-12-15 12:45:20

本文作者:得少


Spring Cloud Config 規範

首先Spring Cloud 是基於 Spring 來擴展的,Spring 本身就提供當創建一個Bean時可從Environment 中將一些屬性值通過@Value的形式注入到業務代碼中的能力。那Spring Cloud Config 要解決的問題就是:

  1. 如何將配置到 Environment 。

  2. 配置變更時,如何控制 Bean 是否需要 create,重新觸發一次 Bean 的初始化,才能將 @Value 註解指定的字段從 Environment 中重新注入。

  3. 配置變更時,如何控制新的配置會更新到 Environment 中,才能保證配置變更時可注入最新的值。

要解決以上三個問題:Spring Cloud Config 規範中剛好定義了核心的三個接口:

  1. PropertySourceLocator:抽象出這個接口,就是讓用戶可定製化的將一些配置加載到 Environment。這部分的配置獲取遵循了 Spring Cloud Config 的理念,即希望能從外部儲存介質中來 loacte。

  2. RefreshScope: Spring Cloud 定義這個註解,是擴展了 Spring 原有的 Scope 類型。用來標識當前這個 Bean 是一個refresh 類型的 Scope。其主要作用就是可以控制 Bean 的整個生命週期。

  3. 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 的處理過程中有一步非常關鍵的操作如下所示:


  1. private ConfigurableApplicationContext bootstrapServiceContext(

  2.            ConfigurableEnvironment environment, final SpringApplication application,

  3.            String configName) {

  4.        //省略

  5.        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();

  6.        // Use names and ensure unique to protect against duplicates

  7.        List<String> names = new ArrayList<>(SpringFactoriesLoader

  8.                .loadFactoryNames(BootstrapConfiguration.class, classLoader));

  9.        //省略

  10.    }


這是 Spring 的工廠加載機制,可通過在 META-INF/spring.factories 文件中配置一些程序中預定義的一些擴展點。比如 Spring Cloud 這裡的實現,可以看到 BootstrapConfiguration 不是一個具體的接口,而是一個註解。

通過這種方式配置的擴展點好處是不侷限於某一種接口的實現,而是同一類別的實現。可以查看 spring-cloud-context 包中的 spring.factories 文件關於BootstrapConfiguration的配置,有一個比較核心入口的配置就是:


  1. org.springframework.cloud.bootstrap.BootstrapConfiguration=\

  2. 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 配置文件中可以看到如下配置:


  1. org.springframework.cloud.bootstrap.BootstrapConfiguration=\

  2. org.springframework.cloud.alibaba.nacos.NacosConfigBootstrapConfiguration


在 Bootstrap 階段初始化的過程中,會獲取所有 ApplicationContextInitializer 類型的 Bean,並設置回SpringApplication主流程當中。如下 BootstrapApplicationListener 類中的部分代碼所示:


  1. private void apply(ConfigurableApplicationContext context,

  2.            SpringApplication application, ConfigurableEnvironment environment) {

  3.        @SuppressWarnings("rawtypes")

  4.        //這裡的 context 是一個 bootstrap 級別的 ApplicationContext,這裡已經含有了在 bootstrap階段所有需要初始化的 Bean。

  5.        //因此可以獲取 ApplicationContextInitializer.class 類型的所有實例

  6.        List<ApplicationContextInitializer> initializers = getOrderedBeansOfType(context,

  7.                ApplicationContextInitializer.class);

  8.        //設置回 SpringApplication 主流程當中

  9.        application.addInitializers(initializers

  10.                .toArray(new ApplicationContextInitializer[initializers.size()]));


  11.        //省略...

  12.    }


這樣一來,就可以通過在 SpringApplication 的主流程中來回調這些ApplicationContextInitializer 的實例,做一些初始化的操作。如下 SpringApplication 類中的部分代碼所示:


  1. private void prepareContext(ConfigurableApplicationContext context,

  2.            ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,

  3.            ApplicationArguments applicationArguments, Banner printedBanner) {

  4.        context.setEnvironment(environment);

  5.        postProcessApplicationContext(context);

  6.        //回調在BootstrapApplicationListener中設置的ApplicationContextInitializer實例

  7.        applyInitializers(context);

  8.        listeners.contextPrepared(context);

  9.        //省略...

  10.    }


  11.    protected void applyInitializers(ConfigurableApplicationContext context) {

  12.        for (ApplicationContextInitializer initializer : getInitializers()) {

  13.            Class requiredType = GenericTypeResolver.resolveTypeArgument(

  14.                    initializer.getClass(), ApplicationContextInitializer.class);

  15.            Assert.isInstanceOf(requiredType, context, "Unable to call initializer.");

  16.            initializer.initialize(context);

  17.        }

  18.    }


在 applyInitializers 方法中,會觸發 PropertySourceBootstrapConfiguration 中的 initialize 方法。如下所示:


  1. @Override

  2.    public void initialize(ConfigurableApplicationContext applicationContext) {

  3.        CompositePropertySource composite = new CompositePropertySource(

  4.                BOOTSTRAP_PROPERTY_SOURCE_NAME);

  5.        AnnotationAwareOrderComparator.sort(this.propertySourceLocators);

  6.        boolean empty = true;

  7.        ConfigurableEnvironment environment = applicationContext.getEnvironment();

  8.        for (PropertySourceLocator locator : this.propertySourceLocators) {

  9.            PropertySource source = null;

  10.            //回調所有實現PropertySourceLocator接口實例的locate方法,

  11.            source = locator.locate(environment);

  12.            if (source == null) {

  13.                continue;

  14.            }


  15.            composite.addPropertySource(source);

  16.            empty = false;

  17.        }

  18.        if (!empty) {

  19.        //從當前Enviroment中獲取 propertySources

  20.            MutablePropertySources propertySources = environment.getPropertySources();

  21.            //省略...

  22.            //將composite中的PropertySource添加到當前應用上下文的propertySources中

  23.            insertPropertySources(propertySources, composite);

  24.            //省略...

  25.        }


在這個方法中會回調所有實現 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的方法主要做了兩件事:

  1. 觸發PropertySourceLocator的locator方法,需要加載最新的值,並替換 Environment 中舊值

  2. Bean中的引用配置值需要重新注入一遍。重新注入的流程是在Bean初始化時做的操作,那也就是需要將refresh scope中的Bean 緩存失效,當再次從refresh scope中獲取這個Bean時,發現取不到,就會重新觸發一次Bean的初始化過程。

這兩個操作所對應的代碼如下所示:


  1. public synchronized Set<String> refresh() {

  2.        Map<String, Object> before = extract(

  3.                this.context.getEnvironment().getPropertySources());

  4.        //1、加載最新的值,並替換Envrioment中舊值

  5.        addConfigFilesToEnvironment();

  6.        Set<String> keys = changes(before,

  7.                extract(this.context.getEnvironment().getPropertySources())).keySet();

  8.        this.context.publishEvent(new EnvironmentChangeEvent(context, keys));

  9.        //2、將refresh scope中的Bean 緩存失效:

  10.        this.scope.refreshAll();

  11.        return keys;

  12.    }


addConfigFilesToEnvironment 方法中發生替換的代碼如下所示:


  1. ConfigurableApplicationContext addConfigFilesToEnvironment() {

  2.        ConfigurableApplicationContext capture = null;

  3.        try {

  4.            //省略...

  5.            //1、這裡會重新觸發PropertySourceLoactor的locate的方法,獲取最新的外部化配置

  6.            capture = (SpringApplicationBuilder)builder.run();


  7.            MutablePropertySources target = this.context.getEnvironment()

  8.                    .getPropertySources();

  9.            String targetName = null;

  10.            for (PropertySource source : environment.getPropertySources()) {

  11.                String name = source.getName();

  12.                //省略..


  13.                //只有不是標準的 Source 才可替換

  14.                if (!this.standardSources.contains(name)) {

  15.                    if (target.contains(name)) {

  16.                        //開始用新的PropertySource替換舊值

  17.                        target.replace(name, source);

  18.                    }

  19.                    //

  20.                }

  21.            }

  22.        }

  23.        //

  24.        return capture;

  25.    }


this.scope.refreshAll() 清空緩存的操作代碼如下所示:


  1. @Override

  2.    public void destroy() {

  3.        List<Throwable> errors = new ArrayList<Throwable>();

  4.        //清空Refresh Scope 中的緩存

  5.        Collection<BeanLifecycleWrapper> wrappers = this.cache.clear();

  6.        //省略...

  7.    }


為了驗證每次配置刷新時,Bean 是新創建的,特意寫了一個Demo 驗證了下,如下所示:


  1. Acm Properties: beijing-region

  2. //刷新前

  3. Object Instance is :com.alibaba.demo.normal.ConfigProperties@1be9634

  4. 2018-11-01 19:16:32.535  INFO 27254 --- [gPullingdefault] startup date [Thu Nov 01 19:16:32 CST 2018]; root of context hierarchy

  5. Acm Properties: qingdao-region

  6. //刷新後

  7. 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的分析請參考:

https://www.cnblogs.com/noahsark/p/spring-scope-analysis.html

後續

關於ContextRefresh 和 RefreshScope的初始化配置是在RefreshAutoConfiguration類中完成的。而RefreshAutoConfiguration類初始化的入口是在spring-cloud-context中的META-INF/spring.factories中配置的。從而完成整個和動態刷新相關的Bean的初始化操作。

推薦程序員必備微信號 


程序員內參
微信號:

programmer0001



推薦理由:
在這裡,我們分享程序員相關技術,職場生活,行業熱點資訊。不定期還會分享IT趣文和趣圖。這裡屬於我們程序員自己的生活,工作和娛樂空間。


 ▼長按下方↓↓↓二維碼識別關注
閱讀原文

TAGS: