[享學Netflix] 十五、Archaius和Spring Cloud的集成
- 2020 年 3 月 18 日
- 筆記
學技術不僅僅是要學技術本身,還有其思想,更重要是它的發展歷史脈絡。因為熟悉了這些,你便會從哲學的角度去思考問題,從而對其它技術也能觸類旁通。 程式碼下載地址:https://github.com/f641385712/netflix-learning
目錄
前言
截止到上篇文章,其實關於Netflix Archaius
的內容都已經講述完了,理論上你現在應該可以沒有障礙的使用它了。
本來本文我是沒有打算去寫的,因為掌握了核心後,去集成任何技術都是不算太難的一件事。但是,但是,但是,奈何Spring Boot/Cloud
如此之火,現在一門技術如果不談和它的整合,都不好意思說自己出道了。
基於此,本文就接著介紹下Netflix Archaius
它和Spring Cloud的整合工程:spring-cloud-starter-netflix-archaius
。
正文
在閱讀接下來內容,請務必確保你已經了解了Netflix Archaius
的核心知識,以及Spring Cloud
的基礎支援:特別是Spring Cloud Context以及它的Commons抽象~。
說明:關於
Netflix Archaius
核心知識,你可以從 上篇文章(也就是十四、十三…)去了解。
spring-cloud-starter-netflix-archaius
首先需要明確:整合方面是Spring Cloud官方去整合Netflix Archaius
,所以它屬於一個官方支援的項目。那麼一切就從它的官方工程說起:
GAV如下:
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-archaius</artifactId> <version>2.2.1.RELEASE</version> </dependency>
從1.4.0.RELEASE
(2017.11)至今,當前最新版本為2.2.1.RELEASE
,該Jar攜帶如下內容:

我本人有個疑問:為毛它會把spring-cloud-netflix-ribbon
帶進來,卻又其實並沒有任何地方使用到它,畢竟archaius
屬於更為底層的基礎不可能使用上層API。 反倒而其實spring-cloud-netflix-ribbon
它自己是攜帶spring-cloud-netflix-archaius
的,所以這個starter的依賴管理得著實讓我費解,若有可解釋得通的朋友,歡迎你留言相告~
另外順便還解答一個小疑問:工程名為何不叫spring-boot-starter-netflix-archaius
,而叫spring-cloud-xxx
呢?我找到了此唯一原因:它使用到了org.springframework.cloud.context.environment.EnvironmentChangeEvent
這個Spring Cloud
的標準事件,從而成功和Spring Cloud整合,所以它必須構建在Spring Cloud下,而非Spring Boot。
spring-cloud-starter-netflix-archaius VS spring-cloud-netflix-archaius
他倆的GAV如下,非常相似有木有。
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-archaius</artifactId> <version>2.2.1.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-netflix-archaius</artifactId> <version>2.2.1.RELEASE</version> </dependency>
這個問題出自於不止一個小夥伴的提問,因此這裡我也順道解釋一波。從表象上看,我相信你藉助IDEA就能看出來:spring-cloud-starter-netflix-archaius
管理著spring-cloud-netflix-archaius
以及其它相關依賴。
為了授之以漁,此處我額外用兩點幫你區分這一類問題而不止是這一個問題:
Spring Boot
官方推薦你自定義的stater至少有兩個模組autoconfigure
模組:包含自動配置的程式碼starter
模組:提供對autoconfigure模組的依賴,以及一些其它的依賴
starter
模組一般無程式碼,是個空jar。它唯一的目的是提供這個庫所必須的依賴(就是管理依賴用的)
官方自己的starter均遵循此規律來實現,譬如:
spring-boot-starter
和spring-boot
spring-boot-starter-actuator
和spring-boot-actuator
spring-boot-starter-aop
和spring-aop + aspectjweaver...
- …
所以,spring-cloud-starter-netflix-archaius
它包含有spring-cloud-netflix-archaius
以及其它依賴,作為starter它只是幫你管理著那些必須依賴而已,而實際幹事的是spring-cloud-netflix-archaius
模組內的Java文件。
spring-cloud-netflix-archaius
它依賴於archaius-core 0.7.6
實現的動態配置管理,其它依賴項均交給spring-cloud-starter-netflix-archaius
管理著。
該Jar的內容並不多,有且僅有4個類:

ConfigurableEnvironmentConfiguration
它是對Spring環境抽象org.springframework.core.env.ConfigurableEnvironment
的一個包裝,適配為org.apache.commons.configuration.AbstractConfiguration
,從而便可和Archaius
無縫整合。
public class ConfigurableEnvironmentConfiguration extends AbstractConfiguration { // 管理著Spring的所有屬性們 private final ConfigurableEnvironment environment; public ConfigurableEnvironmentConfiguration(ConfigurableEnvironment environment) { this.environment = environment; } ... @Override public boolean isEmpty() { return !getKeys().hasNext(); } @Override public boolean containsKey(String key) { return this.environment.containsProperty(key); } @Override public Object getProperty(String key) { return this.environment.getProperty(key); } // 拿到所有的屬性源PropertySource出來,注意這裡需要處理CompositePropertySource這種喲 @Override public Iterator<String> getKeys() { List<String> result = new ArrayList<>(); for (Map.Entry<String, PropertySource<?>> entry : getPropertySources().entrySet()) { PropertySource<?> source = entry.getValue(); if (source instanceof EnumerablePropertySource) { EnumerablePropertySource<?> enumerable = (EnumerablePropertySource<?>) source; for (String name : enumerable.getPropertyNames()) { result.add(name); } } } return result.iterator(); } ... }
該類的唯一作用:把Spring的環境抽象ConfigurableEnvironment
適配為一個Configuration
,從而可以加入到全局配置裡面去。
說明:很多小夥伴看到它最終會被
ArchaiusAutoConfiguration
配置為一個Bean放到容器內,其實那是沒有必要的,問下會解釋緣由。
ArchaiusDelegatingProxyUtils
工具類。代理了ApplicationContext#getBean()
等方法~
public final class ArchaiusDelegatingProxyUtils { // Application作為屬性的key名稱 public static String APPLICATION_CONTEXT = ApplicationContext.class.getName(); // 簡單的說:就是去ApplicationContext裡面去getBean // 前提是:ApplicationContext實例必須在全局的Configuration裡面了~ public static <T> T getNamedInstance(Class<T> type, String name) { ApplicationContext context = (ApplicationContext) ConfigurationManager.getConfigInstance().getProperty(APPLICATION_CONTEXT); return context != null && context.containsBean(name) ? context.getBean(name, type) : null; } // name = prefix + type.getSimpleName(); public static <T> T getInstanceWithPrefix(Class<T> type, String prefix) { String name = prefix + type.getSimpleName(); return getNamedInstance(type, name); } // 調用此方法:可以把ApplicationContext實例,放進Configuration裡面 public static void addApplicationContext(ConfigurableApplicationContext context) { AbstractConfiguration config = ConfigurationManager.getConfigInstance(); config.clearProperty(APPLICATION_CONTEXT); config.setProperty(APPLICATION_CONTEXT, context); } }
注意:以上三個工具方法,默認情況下沒有被任何地方用到,所以當你需要自行擴展的時候,可以使用。
ArchaiusEndpoint
用於訪問Archaius
的配置的端點:有且僅有一個讀方法,並無寫方法
@Endpoint(id = "archaius") public class ArchaiusEndpoint { // 只有一個讀方法而已 @ReadOperation public Map<String, Object> invoke() { Map<String, Object> map = new LinkedHashMap<>(); AbstractConfiguration config = ConfigurationManager.getConfigInstance(); if (config instanceof ConcurrentCompositeConfiguration) { ConcurrentCompositeConfiguration composite = (ConcurrentCompositeConfiguration) config; for (Configuration item : composite.getConfigurations()) { append(map, item); } } else { append(map, config); } return map; } // 核心在append方法,往Map里放值 // 需要注意的是:前面三種屬性源都是不會添加進去的哦,就連ConfigurableEnvironmentConfiguration都不給你訪問 private void append(Map<String, Object> map, Configuration config) { if (config instanceof ConfigurableEnvironmentConfiguration) return; if (config instanceof SystemConfiguration) return; if (config instanceof EnvironmentConfiguration) return; for (Iterator<String> iter = config.getKeys(); iter.hasNext();) { String key = iter.next(); map.put(key, config.getProperty(key)); } } }
訪問示例:http://localhost:8080/actuator/archaius
,默認情況下返回值是{}
(因為你並沒有手動給它設過值嘛),下面給給添加幾個值:
public static void main(String[] args) { AbstractConfiguration config = ConfigurationManager.getConfigInstance(); config.addProperty("name", "YourBatman"); config.addProperty("age", 18); ConfigurableApplicationContext context = SpringApplication.run(CloudFeignApplication.class, args); // 這算是一個Bug,當把Context放進去後,如果訪問`archaius`端點,會報錯:序列化異常,所以此處展示注釋掉嘍 // ArchaiusDelegatingProxyUtils.addApplicationContext(context); }
運行程式,再次訪問http://localhost:8080/actuator/archaius
,控制台輸出:
{"name":"YourBatman","age":18}
完美~
說明:此端點會有序列化的過程,所以若你存在不能被序列化的屬性,此端點就會拋錯哦。比如文上說的ApplicationContext就不能被正常序列化~
ArchaiusAutoConfiguration
以上3個類均是獨立的API,有且僅有本來和Spring Boot/Cloud
打了交道。它被配置在當前工程的spring.factories
文件里,啟動時生效:
org.springframework.boot.autoconfigure.EnableAutoConfiguration= org.springframework.cloud.netflix.archaius.ArchaiusAutoConfiguration
關於它的詳解,請閱讀源碼注釋處:
// 說明:ConfigurationBuilder是Apache Commons Configuration1.x的核心API // ConcurrentCompositeConfiguration是archaius1.x(0.x)的核心API @Configuration(proxyBeanMethods = false) @ConditionalOnClass({ ConcurrentCompositeConfiguration.class, ConfigurationBuilder.class }) @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE) //配置的自動配置,優先順序最高 public class ArchaiusAutoConfiguration { // 避免多次初始化的開關 private static final AtomicBoolean initialized = new AtomicBoolean(false); // Spring容器的環境 @Autowired private ConfigurableEnvironment env; // 由此可知:你若要擴展屬性Configuration,直接往容器里扔一個Bean即可~~~非常方便 @Autowired(required = false) private List<AbstractConfiguration> externalConfigurations = new ArrayList<>(); // 用於載入Archaius的那些默認行為,比如類路徑下的config.properties private static DynamicURLConfiguration defaultURLConfig; // 這是核心初始化流程:會把容器內的配置都放在一起,並且做一些初始化的動作 // 把用戶自定義的externalConfigurations都放進來 @Bean public static ConfigurableEnvironmentConfiguration configurableEnvironmentConfiguration(ConfigurableEnvironment env, ApplicationContext context) { ConfigurableEnvironmentConfiguration envConfig = new ConfigurableEnvironmentConfiguration(env); configureArchaius(envConfig, env, externalConfigurations); return envConfig; } protected static void configureArchaius(...) { // 保證只初始化一次 if (initialized.compareAndSet(false, true)) { // 把應用名稱,appName放進系統屬性(全局配置) String appName = env.getProperty("spring.application.name"); if (appName == null) { appName = "application"; log.warn("No spring.application.name found, defaulting to 'application'"); } System.setProperty(DeploymentContext.ContextKey.appId.getKey(), appName); // 請注意:它是一個組合屬性,將會組合下面的一些配置們 ConcurrentCompositeConfiguration config = new ConcurrentCompositeConfiguration(); ... // 把用戶額外配置的屬性都放進來 config.addConfiguration(externalConfig); // 把Spring Env屬性放進來 config.addConfiguration(envConfig, ConfigurableEnvironmentConfiguration.class.getSimpleName()); // 把形如config.properties這種配置加進來(默認是empty哦) defaultURLConfig = new DynamicURLConfiguration(); config.addConfiguration(defaultURLConfig, URL_CONFIG_NAME); // 如果系統屬性、系統環境沒有被禁止 // 那就把new SystemConfiguration()、 new EnvironmentConfiguration()加進來 if (!Boolean.getBoolean(DISABLE_DEFAULT_SYS_CONFIG)) if (!Boolean.getBoolean(DISABLE_DEFAULT_ENV_CONFIG)) // 把組合好的配置,安裝到全局的Configuration裡面去 // 需要注意的是:這裡install是指定了外部config的 // 所以即使你內部`ConfigurationManager#createDefaultConfigInstance`過,最終也會被這個給替換掉 // 但其裡面的內容都會被copy過來,不會丟失哦~~~~ // 所以這個install是專門用於設置外部config進來的~~~~ ... ConfigurationManager.install(config); // addArchaiusConfiguration(config); } } ... @Bean @ConditionalOnEnabledEndpoint protected ArchaiusEndpoint archaiusEndpoint() { return new ArchaiusEndpoint(); } ... }
以上初始化步驟看似複雜,但其實只做了一件事:將Spring環境配置、用戶自定義的配置externalConfigurations
,以及你已經放進去的一些屬性們全部放進全局Configuration里。
它的初始化步驟可總結如下:
- 把應用名
appName
放進去(應用名由spring.application.name
來定,否則默認值是application) - 把用戶自定義的的
Configuration
Bean放進去(因為它最先放進去,所以優先順序最高) - 把Spring的
ConfigurableEnvironmentConfiguration
放進去(優先順序第二) - 把
Archaius
自己的DynamicURLConfiguration
放進去 - 把
SystemConfiguration、EnvironmentConfiguration
放進去(若沒禁止的話)
最終,全局Configuration
屬性的值的截圖參考如下:

初始化結束後,不僅僅完成了全局Configuration
的初始化,並且然後還把ConfigurableEnvironmentConfiguration
放進了容器(其實我覺得它是完全沒必要放進容器里的,因為它裡面的屬性並不全,我們也不會直接用它:只有Spring容器內的,向你通過ConfigurationManager.getConfigInstance().addProperty("name", "YourBatman")
這種方式放進去的話它是獲取不到的哦)。
全局配置如何感知到Spring環境屬性的變更
在使用開發中,我們的配置大都寫在application.properties/yaml
里,或者在配置中心裡(而並不會放在conifg.properties
里),總之最終都會被放進Spring 的Enviroment
里,那麼問題就來了:全局配置如何感知到Spring環境屬性的變更,從而保持同步性呢?
這時候Spring Cloud
就出馬了,利用org.springframework.cloud.context.environment.EnvironmentChangeEvent
這個事件就能很好的完成這個工作:
ArchaiusAutoConfiguration: // 它是個ApplicationListener,監聽EnvironmentChangeEvent事件 @Configuration(proxyBeanMethods = false) @ConditionalOnProperty(value = "archaius.propagate.environmentChangedEvent", matchIfMissing = true) @ConditionalOnClass(EnvironmentChangeEvent.class) protected static class PropagateEventsConfiguration implements ApplicationListener<EnvironmentChangeEvent> { @Autowired private Environment env; @Override public void onApplicationEvent(EnvironmentChangeEvent event) { // 拿到全局配置 AbstractConfiguration manager = ConfigurationManager.getConfigInstance(); // 從事件里拿到所有改變了的Key們,一個一個的處理 for (String key : event.getKeys()) { // 拿到註冊在Configuration上的所有事件門,一個一個的觸發他們的configurationChanged方法 // 事件類型是:AbstractConfiguration.EVENT_SET_PROPERTY; for (ConfigurationListener listener : manager.getConfigurationListeners()) { Object source = event.getSource(); // TODO: Handle add vs set vs delete? int type = AbstractConfiguration.EVENT_SET_PROPERTY; // 改變後的value從env里獲取(可能為null哦~) // 當然一般建議動態修改,而非刪除,請務必注意嘍 String value = this.env.getProperty(key); boolean beforeUpdate = false; listener.configurationChanged(new ConfigurationEvent(source, type, key, value, beforeUpdate)); } } } }
邏輯很簡單:對所有發生變更的key們,逐個觸發其AbstractConfiguration.EVENT_SET_PROPERTY
事件從而同步更新全局配置屬性。
另外,你可以通過archaius.propagate.environmentChangedEvent=false
來顯示的關閉這個行為,但很顯然一般你並不需要這麼做。
使用示例
使用示例在Spring Cloud
配置中心篇章里會回溯到此,請出門參閱。
關於Archaius2.x
其實Archaius1.x
(或者說0.x)現在基本處於一個停更(只維護)狀態,一般來說軟體到這種時候,生命的盡頭就快到了。
說明:
Archaius1.x
的最新版本是0.7.7(和0.7.6差不多)
而實際上Archaius
是在繼續發展2.x版本的:
<dependency> <groupId>com.netflix.archaius</groupId> <artifactId>archaius2-core</artifactId> <version>2.3.16</version> </dependency>
它採用了全新的API設計(不向下加絨),並且採用API + 實現分離的方式,並不強依賴於Commons Configuration
來實現,可擴展性更強了。
Archaius2.x
雖然優點眾多,但是,但是,但是:由於不管是現在的Hystrix
,還是Spring Cloud Netflix xxx
(哪怕到了最新版本)依賴的均是Archaius1.x
版本(0.7.7版本),所以本系列只講1.x,而不講2.x。
還是一樣的,萬變不離其宗,有興趣的小夥伴可自行研究Archaius2.x
如何使用?
總結
關於Netflix Archaius和Spring Cloud的集成部分就說到這了,至此全部關於Archaius
的內容就介紹完了,它作為基礎中的基礎,後面章節將會使用到它,所以還會頻繁見面哦~
另外提示一點:你可以看到,即便到了Spring Boot2.2.x
這麼高的版本,它依賴的依舊還都是Archaius 1.x
版本以及Commons Configuration1.x版本。所以說,即使Commons Configuration2.x
已成為主流被推薦使用,但是1.x依舊有很高的學習價值的,請勿忽視它哦。