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

需求

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

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

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

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

背景和問題

我們的服務(wù)原來有一批單機的配置诵叁,由于同一 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 屬性內(nèi)势腮。

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

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

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

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

環(huán)境和屬性

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

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

但 Spring 管理配置的最小單位并不是屬性劳澄,而是PropertySource?(屬性源)地技,我們可以理解 PropertySource 是一個文件,或是某張配置數(shù)據(jù)表,Spring 在 Environment 內(nèi)維護(hù)一個 PropertySourceList逗爹,當(dāng)我們獲取配置時童社,Spring 從這些 PropertySource 內(nèi)查找到對應(yīng)的值,并使用ConversionService?將值轉(zhuǎn)換為對應(yīng)的類型返回趣苏。

Spring Cloud 配置刷新機制

分布式配置

Spring Cloud 內(nèi)提供了PropertySourceLocator?接口來對接 Spring 的 PropertySource 體系,通過 PropertySourceLocator梯轻,我們就拿到一個”自定義”的 PropertySource食磕,Spring Cloud 里還有一個實現(xiàn)ConfigServicePropertySourceLocator?,通過它喳挑,我們可以定義一個遠(yuǎn)程的 ConfigService彬伦,通過公用這個 ConfigService 來實現(xiàn)分布式的配置服務(wù)滔悉。

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

scope 配置刷新

遠(yuǎn)程配置有了,接下來就是對變化的監(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 的銷毀會導(dǎo)致 scope 內(nèi)(被 RefreshScope 注解)所有的 bean 都會被銷毀。而這些被強制設(shè)置為 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)境內(nèi)所有 PropertySource 內(nèi)的配置后肖方,重新創(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;

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

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

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

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

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

我們知道卖鲤,Spring 的 Bean 都是在 BeanFactory 內(nèi)創(chuàng)建的肾扰,創(chuàng)建邏輯的入口在AbstractBeanFactory.doGetBean(name, requiredType, args, false)?方法畴嘶,而具體實現(xiàn)在AbstractAutowireCapableBeanFactory.doCreateBean?方法內(nèi),在這個方法里集晚,實現(xiàn)了 Bean 實例的創(chuàng)建窗悯、屬性填充、初始化方法調(diào)用等邏輯偷拔。

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

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

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

找到對應(yīng)的屬性后顷啼,再使用 converter 將屬性轉(zhuǎn)換為對應(yīng)的類型注入到 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)屬性注入的關(guān)鍵點躬厌,再從這些關(guān)鍵點里找可修改點。

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

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

Spring 會在解決類屬性注入時,使用 PropertyResolver 將配置項解析為類屬性指定的類型荒揣。這時候添加屬性解析器 PropertyResolver 或類型轉(zhuǎn)換器 ConversionService 可以插手屬性的操作篷角。但它們都只負(fù)責(zé)處理一個屬性,由于我的目標(biāo)是”多個”屬性變成一個屬性乳附,它們也無能為力内地。

我這里能想到的方式是借用 Spring 自動注入的能力伴澄,把 Environment Bean 注入到某個類中,然后在類的初始化方法里對 Environment 內(nèi)的 PropertySource 里進(jìn)行修改阱缓,也可以達(dá)成目的非凌,這里貼一下偽代碼。

@Component@RefreshScope// 借用 Spring Cloud 實現(xiàn)此 Bean 的刷新publicclassListSupportPropertyResolver{@AutowiredConfigurableEnvironment env;// 將環(huán)境注入到 Bean 內(nèi)是修改環(huán)境的重要前提@PostConstructpublicvoidinit() {// 將屬性鍵值對從環(huán)境內(nèi)取出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 內(nèi)propertySources.addFirst(newMapPropertySource("modifiedProperties", propertiesMap));? ? }}

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

當(dāng)然了喉悴,有了比較”正規(guī)”的方式后,我們不必要對 PropertySource 進(jìn)行修改玖媚,畢竟全局修改等于未知風(fēng)險或埋坑箕肃。

小結(jié)

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末涩维,一起剝皮案震驚了整個濱河市殃姓,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌瓦阐,老刑警劉巖蜗侈,帶你破解...
    沈念sama閱讀 216,496評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異垄分,居然都是意外死亡宛篇,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評論 3 392
  • 文/潘曉璐 我一進(jìn)店門薄湿,熙熙樓的掌柜王于貴愁眉苦臉地迎上來叫倍,“玉大人,你說我怎么就攤上這事豺瘤∵壕耄” “怎么了?”我有些...
    開封第一講書人閱讀 162,632評論 0 353
  • 文/不壞的土叔 我叫張陵坐求,是天一觀的道長蚕泽。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么须妻? 我笑而不...
    開封第一講書人閱讀 58,180評論 1 292
  • 正文 為了忘掉前任仔蝌,我火速辦了婚禮,結(jié)果婚禮上荒吏,老公的妹妹穿的比我還像新娘敛惊。我一直安慰自己,他們只是感情好绰更,可當(dāng)我...
    茶點故事閱讀 67,198評論 6 388
  • 文/花漫 我一把揭開白布瞧挤。 她就那樣靜靜地躺著,像睡著了一般儡湾。 火紅的嫁衣襯著肌膚如雪特恬。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,165評論 1 299
  • 那天徐钠,我揣著相機與錄音癌刽,去河邊找鬼。 笑死丹皱,一個胖子當(dāng)著我的面吹牛妒穴,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播摊崭,決...
    沈念sama閱讀 40,052評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼杰赛!你這毒婦竟也來了呢簸?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,910評論 0 274
  • 序言:老撾萬榮一對情侶失蹤乏屯,失蹤者是張志新(化名)和其女友劉穎根时,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體辰晕,經(jīng)...
    沈念sama閱讀 45,324評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡蛤迎,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,542評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了含友。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片替裆。...
    茶點故事閱讀 39,711評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖窘问,靈堂內(nèi)的尸體忽然破棺而出辆童,到底是詐尸還是另有隱情,我是刑警寧澤惠赫,帶...
    沈念sama閱讀 35,424評論 5 343
  • 正文 年R本政府宣布把鉴,位于F島的核電站,受9級特大地震影響儿咱,放射性物質(zhì)發(fā)生泄漏庭砍。R本人自食惡果不足惜场晶,卻給世界環(huán)境...
    茶點故事閱讀 41,017評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望怠缸。 院中可真熱鬧诗轻,春花似錦、人聲如沸凯旭。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽罐呼。三九已至鞠柄,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間嫉柴,已是汗流浹背厌杜。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留计螺,地道東北人夯尽。 一個月前我還...
    沈念sama閱讀 47,722評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像登馒,于是被迫代替她去往敵國和親匙握。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,611評論 2 353

推薦閱讀更多精彩內(nèi)容