[享學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以及其它相關依賴

為了授之以漁,此處我額外用兩點幫你區分這一類問題而不止是這一個問題:

  1. Spring Boot官方推薦你自定義的stater至少有兩個模組
    1. autoconfigure模組:包含自動配置的程式碼
    2. starter模組:提供對autoconfigure模組的依賴,以及一些其它的依賴
  2. starter模組一般無程式碼,是個空jar。它唯一的目的是提供這個庫所必須的依賴(就是管理依賴用的)

官方自己的starter均遵循此規律來實現,譬如:

  • spring-boot-starterspring-boot
  • spring-boot-starter-actuatorspring-boot-actuator
  • spring-boot-starter-aopspring-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里

它的初始化步驟可總結如下:

  1. 把應用名appName放進去(應用名由spring.application.name來定,否則默認值是application)
  2. 用戶自定義的的Configuration Bean放進去(因為它最先放進去,所以優先順序最高)
  3. 把Spring的ConfigurableEnvironmentConfiguration放進去(優先順序第二)
  4. Archaius自己的DynamicURLConfiguration放進去
  5. 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依舊有很高的學習價值的,請勿忽視它哦。