zookeeper結合PropertyPlaceholderConfigurer實現的統一配置組件,巧妙的應用了PropertyPlaceholderConfigurer搜索多種數據源的優勢,且對原有代碼沒有任務的侵入性。
之前我的2015下半年總結中有提到我們的項目採用了微服務的模式,也就是說系統按一定的技術以及業務切分成各個獨立的小系統,比如我們的產品是一個電商系統,那麼可以分為:前端WAP,前端api,商品管理系統,採購系統,主數據管理系統,用戶中心管理,價格管理系統,促銷管理系統,訂單管理系統,庫存管理系統,門店管理系統等等,最後統計的數據是dubbo服務就高達18個,web系統有3個,前端WAP站點一個。這些系統要想跑起來就需要連接各種資源,比如服務地址,資料庫,緩存,文件系統,消息隊列等,一般項目中使用到的配置項大致是如下兩類:資源以及具體業務相關。
配置中心的應用場景:
- 公司記憶體在多個系統,比如我們的web站點外加dubbo服務總超過20個,且系統之間的技術架構基本相同並且有一定的聯繫性
- 一套系統需要配置多個環境,我們有開發環境,測試環境,預上線環境,線上環境
配置中心需要解決的核心問題是多個系統配置信息統一管理困難的問題,這裡我關心的功能如下:
- 從zookeeper中載入數據到bean管理器中
- 解決多環境取值問題,開發環境,測試環境,生產環境
- zookeeper配置與本地配置相容問題,通過一定手段可決定是使用zookeeper信息還是本地信息,比如本地調試時非常有用
- zookeeper配置項發生變更後的更新問題
這裡貼一張百度的disconf圖,這個項目的功能更加強大,有興趣可去研究:
首先我們看下系統中是如何使用的配置項,一般有兩種用法:
a:某些XML配置文件中,比如:
<dubbo:protocol accesslog="true" name="dubbo" port="${zk.port}" />
b:程式中,一般是通過@Value這個註解來獲取,比如我們可以寫一個配置類來載入配置項:
@Service public class MmsConfig { @Value("${es.cluster.name}") private String esClusterName; public String getEsClusterName() { return esClusterName; }
這裡的@Value的註解有兩種用法:
- @Value("${es.cluster.name}")
- @Value("#{configProperties['es.cluster.name']}")
要搞清楚上面這兩種用法,需要知道下麵這幾個類:
- PropertiesFactoryBean:這裡官方給出的文檔是:Allows for making a properties file from a classpath location available as Properties instance in a bean factory. Can be used to populate any bean property of type Properties via a bean reference.Supports loading from a properties file and/or setting local properties on this FactoryBean. The created Properties instance will be merged from loaded and local values. If neither a location nor local properties are set, an exception will be thrown on initialization.就是從指定的文檔中讀取配置信息並且載入到系統中,它在程式中可以使用上面的第二種方式。
- BeanFactoryPostProcessor:直接點就是對bean提供了屬性值的管理
- PropertyPlaceholderConfigurer,實現了BeanFactoryPostProcessor介面,這個類比較高級,主要是替換點位符${...},它不光從文件中載入,還從系統變數以及環境變數中搜索相關key
- PreferencesPlaceholderConfigurer,它是PropertyPlaceholderConfigurer的一個子類
搞清楚了系統從配置文件中取值的邏輯,那麼理解統一配置中心就不難了,無非就是在載入配置項的地方做些手腳讓其按照我們的意圖去獲取更新配置項。這裡我們應用一個已經非常成熟的產品zookepper,它的數據結果類似如下:
核心的功能就是從zookepper中獲取配置項然後載入到系統變數中即可。我們看下如果將zookeeper中的配置項載入到系統中,根據PropertyPlaceholderConfigurer的功能描述,它會從三個地方去載入配置,我們選擇將zookeeper配置載入到系統變數中,核心代碼如下兩步:
- 從zookeeper中獲取一個配置項的Map,這裡就不貼代碼了
- 將Map一個一個填充到系統變數中,只要系統變數中有這些值,那麼我們就可以直接按最上面的方式訪問我們的屬性值了
private void setSystemProperys(ConfigCenter cc, Map<String, Object> config) { for(String key:config.keySet()){ String value=cc.get(key); if(key.contains(".")){ key=key.substring(1); } if(value==null) { value=""; } System.setProperty(key, value); } }
不同環境的配置如何解決?
上面的功能只是提到瞭如何將zookepper中的配置載入到系統中,那麼如何根據當前的環境載入正確的配置呢,這裡也只需要在系統啟動時傳遞一個環境變更即可,配置中心根據註入的環境變數值來判斷應該載入哪個環境的數據。如果是非web項目,我們只需要在啟動服務的命令中增加一個環境變更的參數即可:-Dmaven.test.skip=true clean install -Devn=sim,如果是web項目,我們可以通過Servelt配置文件來完成,最終通過ServletContexstListener來獲取參數,流程如下所示:
寫一個自定義的ServletContextListener,它的作用主要是從系統啟動環境中獲取變數,提供給配置中心使用
public class WanmeiContextLoaderListener implements ServletContextListener { @Override public void contextInitialized(ServletContextEvent sce) { String evn = System.getProperty("evn"); if(evn == null || evn.equals("")) { evn = sce.getServletContext().getInitParameter("evn"); if (evn == null) { evn = "qa"; } System.setProperty("evn", evn); } } @Override public void contextDestroyed(ServletContextEvent sce) { // TODO Auto-generated method stub } }
zookeeper中的配置項發生變化後如何更新bean中的值呢?
我們可以利用guava提供的enventbus來解決,訂閱一個zookeeper更新事件去更新系統變更即可,DataChangeEvent是自定義的一個類,要想實現自動更新需要寫一些回調方法,也可以參考下這個項目:https://github.com/jamesmorgan/ReloadablePropertiesAnnotation
DataChangeEvent dataChangeEvent=new DataChangeEvent(map, DataChangeEvent.DataType.REMOTE, DataChangeEvent.ChangeType.DELETE); configOption.getEnventBus().post(dataChangeEvent);
如何配置呢?
只需要在PropertyPlaceholderConfigurer時加了一個depends-on就行,目的是讓其先執行我們的後門程式,其它的使用不受影響,基本不需要修改原有代碼。
<bean id="initSpringProperties" class="config.center.spring.SpringPropertyInjectSupport" lazy-init="false" init-method="init"> <property name="configNameSpaces" value="/configcenter/mms" /> </bean> <bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer" depends-on="initSpringProperties"> <property name="locations"> <list> </list> </property> <property name="fileEncoding" value="UTF-8" /> </bean>
本文引用:
http://www.ibm.com/developerworks/cn/opensource/os-cn-zookeeper/
https://github.com/knightliao/disconf