從 Spring 的環(huán)境到 Spring Cloud 的配置

需求

不知不覺,web 開發(fā)已經(jīng)進入 “微服務”、”分布式” 的時代,致力于提供通用 Java 開發(fā)解決方案的 Spring 自然不甘人后,提出了 Spring Cloud 來擴大 Spring 在微服務方面的影響,也取得了市場的認可,在我們的業(yè)務中也有應用。

前些天,我在一個需求中也遇到了 spring cloud 的相關問題。我們在用的是 Spring Cloud 的 config 模塊,它是用來支持分布式配置的,原來單機配置在使用了 Spring Cloud 之后,可以支持第三方存儲配置和配置的動態(tài)修改和重新加載,自己在業(yè)務代碼里實現(xiàn)配置的重新加載,Spring Cloud 將整個流程抽離為框架,并很好的融入到 Spring 原有的配置和 Bean 模塊內。

雖然在解決需求問題時走了些彎路,但也借此機會了解了 Spring Cloud 的一部分,抽空總結一下問題和在查詢問題中了解到的知識,分享出來讓再遇到此問題的同學少踩坑吧。

本文基于 Spring 5.0.5、Spring Boot 2.0.1 和 Spring Cloud 2.0.2。

背景和問題

我們的服務原來有一批單機的配置,由于同一 key 的配置太長,于是將其配置為數(shù)組的形式,并使用 Spring Boot 的@ConfigurationProperties?和@Value?注解來解析為 Bean 屬性。

properties 文件配置像:

test.config.elements[0]=value1

test.config.elements[1]=value2

test.config.elements[2]=value3

在使用時:

@ConfigurationProperties(prefix="test.config")Class Test{? ? @Value("${#elements}")privateString[]elements;}

這樣,Spring 會對 Test 類自動注入,將數(shù)組 [value1,value2,value3] 注入到 elements 屬性內。

而我們使用 Spring Cloud 自動加載配置的姿勢是這樣:

@RefreshScopeclassTest{@Value("${test.config.elements}")privateString[] elements;}

使用@RefreshScope?注解的類,在環(huán)境變量有變動后會自動重新加載,將最新的屬性注入到類屬性內,但它卻不支持數(shù)組的自動注入。

而我的目標是能找到一種方式,使其即支持注入數(shù)組類型的屬性,又能使用 Spring Cloud 的自動刷新配置的特性。

環(huán)境和屬性

無論Spring Cloud 的特性如何優(yōu)秀,在 Spring 的地盤,還是要入鄉(xiāng)隨俗,和 Spring 的基礎組件打成一片。所以為了了解整個流程,我們就要先了解 Spring 的基礎。

Spring 是一個大容器,它不光存儲 Bean 和其中的依賴,還存儲著整個應用內的配置,相對于 BeanFactory 存儲著各種 Bean,Spring 管理環(huán)境配置的容器就是Environment?,從 Environment 內,我們能根據(jù) key 獲取所有配置,還能根據(jù)不同的場景(Profile,如 dev,test,prod)來切換配置。

但 Spring 管理配置的最小單位并不是屬性,而是PropertySource?(屬性源),我們可以理解 PropertySource 是一個文件,或是某張配置數(shù)據(jù)表,Spring 在 Environment 內維護一個 PropertySourceList,當我們獲取配置時,Spring 從這些 PropertySource 內查找到對應的值,并使用ConversionService?將值轉換為對應的類型返回。

Spring Cloud 配置刷新機制

分布式配置

Spring Cloud 內提供了PropertySourceLocator?接口來對接 Spring 的 PropertySource 體系,通過 PropertySourceLocator,我們就拿到一個”自定義”的 PropertySource,Spring Cloud 里還有一個實現(xiàn)ConfigServicePropertySourceLocator?,通過它,我們可以定義一個遠程的 ConfigService,通過公用這個 ConfigService 來實現(xiàn)分布式的配置服務。

從ConfigClientProperties?這個配置類我們可以看得出來,它也為遠程配置預設了用戶名密碼等安全控制選項,還有 label 用來區(qū)分服務池等配置。

scope 配置刷新

遠程配置有了,接下來就是對變化的監(jiān)測和基于配置變化的刷新。

Spring Cloud 提供了ContextRefresher?來幫助我們實現(xiàn)環(huán)境的刷新,其主要邏輯在refreshEnvironment?方法和scope.refreshAll()?方法,我們分開來看。

我們先來看 spring cloud 支持的 scope.refreshAll 方法。

publicvoidrefreshAll(){super.destroy();this.context.publishEvent(newRefreshScopeRefreshedEvent());}

scope.refreshAll 則更”野蠻”一些,直接銷毀了 scope,并發(fā)布了一個 RefreshScopeRefreshedEvent 事件,scope 的銷毀會導致 scope 內(被 RefreshScope 注解)所有的 bean 都會被銷毀。而這些被強制設置為 lazyInit 的 bean 再次創(chuàng)建時,也就完成了新配置的重新加載。

ConfigurationProperties 配置刷新

然后再回過頭來看 refreshEnvironment 方法。

Map before = extract(this.context.getEnvironment().getPropertySources());addConfigFilesToEnvironment();Set keys = changes(before,extract(this.context.getEnvironment().getPropertySources())).keySet();this.context.publishEvent(newEnvironmentChangeEvent(context, keys));returnkeys;

它讀取了環(huán)境內所有 PropertySource 內的配置后,重新創(chuàng)建了一個 SpringApplication 以刷新配置,再次讀取所有配置項并得到與前面保存的配置項的對比,最后將前后配置差發(fā)布了一個EnvironmentChangeEvent?事件。 而 EnvironmentChangeEvent 的監(jiān)聽器是由 ConfigurationPropertiesRebinder 實現(xiàn)的,其主要邏輯在rebind?方法。

Object bean =this.applicationContext.getBean(name);if(AopUtils.isAopProxy(bean)) {bean = ProxyUtils.getTargetObject(bean);}if(bean !=null) {this.applicationContext.getAutowireCapableBeanFactory().destroyBean(bean);this.applicationContext.getAutowireCapableBeanFactory().initializeBean(bean, name);returntrue;

可以看到它的處理邏輯,就是把其內部存儲的ConfigurationPropertiesBeans?依次執(zhí)行銷毀邏輯,再執(zhí)行初始化邏輯實現(xiàn)屬性的重新綁定。

這里可以知道,Spring Cloud 在進行配置刷新時是考慮過 ConfigurationProperties 的,經(jīng)過測試,在 ContextRefresher 刷新上下文后,ConfigurationProperties 注解類的屬性是會進行動態(tài)刷新的。

測試一次就解決的事情,感覺有些白忙活了。。不過既然查到這里了,就再往下深入一些。

Bean 的創(chuàng)建與環(huán)境

接著我們再來看一下,環(huán)境里的屬性都是怎么在 Bean 創(chuàng)建時被使用的。

我們知道,Spring 的 Bean 都是在 BeanFactory 內創(chuàng)建的,創(chuàng)建邏輯的入口在AbstractBeanFactory.doGetBean(name, requiredType, args, false)?方法,而具體實現(xiàn)在AbstractAutowireCapableBeanFactory.doCreateBean?方法內,在這個方法里,實現(xiàn)了 Bean 實例的創(chuàng)建、屬性填充、初始化方法調用等邏輯。

在這里,有一個非常復雜的步驟就是調用全局的BeanPostProcessor?,這個接口是 Spring 為 Bean 創(chuàng)建準備的勾子接口,實現(xiàn)這個接口的類可以對 Bean 創(chuàng)建時的操作進行修改。它是一個非常重要的接口,是我們能干涉 Spring Bean 創(chuàng)建流程的重要入口。

我們要說的是它的一種具體實現(xiàn)ConfigurationPropertiesBindingPostProcessor?,它通過調用鏈ConfigurationPropertiesBinder.bind() --> Binder.bindObject() --> Binder.findProperty()?方法查找環(huán)境內的屬性。

private ConfigurationProperty findProperty(ConfigurationPropertyName name,Context context) {if(name.isEmpty()) {returnnull;}returncontext.streamSources().map((source) -> source.getConfigurationProperty(name)).filter(Objects::nonNull).findFirst().orElse(null);}

找到對應的屬性后,再使用 converter 將屬性轉換為對應的類型注入到 Bean 骨。

private Object bindProperty(Bindable target, Contextcontext,ConfigurationPropertyproperty) {context.setConfigurationProperty(property);Object result =property.getValue();result =this.placeholdersResolver.resolvePlaceholders(result);result =context.getConverter().convert(result, target);returnresult;}

一種 trick 方式

由上面可以看到,Spring 是支持 @ConfigurationProperties 屬性的動態(tài)修改的,但在查詢流程時,我也找到了一種比較 trick 的方式。

我們先來整理動態(tài)屬性注入的關鍵點,再從這些關鍵點里找可修改點。

PropertySourceLocator 將 PropertySource 從遠程數(shù)據(jù)源引入,如果這時我們能修改數(shù)據(jù)源的結果就能達到目的,可是 Spring Cloud 的遠程資源定位器 ConfigServicePropertySourceLocator 和 遠程調用工具 RestTemplate 都是實現(xiàn)類,如果生硬地對其繼承并修改,代碼很不優(yōu)雅。

Bean 創(chuàng)建時會依次使用 BeanPostProcessor 對上下文進行操作。這時添加一個 BeanPostProcessor,可以手動實現(xiàn)對 Bean 屬性的修改。但這種方式 實現(xiàn)起來很復雜,而且由于每一個 BeanPostProcessor 在所有 Bean 創(chuàng)建時都會調用,可能會有安全問題。

Spring 會在解決類屬性注入時,使用 PropertyResolver 將配置項解析為類屬性指定的類型。這時候添加屬性解析器 PropertyResolver 或類型轉換器 ConversionService 可以插手屬性的操作。但它們都只負責處理一個屬性,由于我的目標是”多個”屬性變成一個屬性,它們也無能為力。

我這里能想到的方式是借用 Spring 自動注入的能力,把 Environment Bean 注入到某個類中,然后在類的初始化方法里對 Environment 內的 PropertySource 里進行修改,也可以達成目的,這里貼一下偽代碼。

@Component@RefreshScope// 借用 Spring Cloud 實現(xiàn)此 Bean 的刷新publicclassListSupportPropertyResolver{@AutowiredConfigurableEnvironment env;// 將環(huán)境注入到 Bean 內是修改環(huán)境的重要前提@PostConstructpublicvoidinit() {// 將屬性鍵值對從環(huán)境內取出Map properties = extract(env.getPropertySources());// 解析環(huán)境里的數(shù)組,抽取出其中的數(shù)組配置Map> listProperties = collectListProperties(properties)Map propertiesMap =newHashMap<>(listProperties);? ? ? ? MutablePropertySources propertySources = env.getPropertySources();// 把數(shù)組配置生成一個 PropertySource 并放到環(huán)境的 PropertySourceList 內propertySources.addFirst(newMapPropertySource("modifiedProperties", propertiesMap));? ? }}

這樣,在創(chuàng)建 Bean 時,就能第一優(yōu)先級使用我們修改過的 PropertySource 了。

當然了,有了比較”正規(guī)”的方式后,我們不必要對 PropertySource 進行修改,畢竟全局修改等于未知風險或埋坑。

小結

查找答案的過程中,我更深刻地理解到 Environment、BeanFactory 這些才是 Spring 的基石,框架提供的各種花式功能都是基于它們實現(xiàn)的,對這些知識的掌握,對于理解它表現(xiàn)出來的高級特性很有幫助,之后再查找框架問題也會更有方向。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容