引言
最近LZ的技術博文數量直線下降,實在是非常抱歉,之前LZ曾信誓旦旦的說一定要把《深入理解計算機系統》寫完,現在看來,LZ似乎是在打自己臉了。盡管LZ內心一直沒放棄,但從現狀來看,需要等LZ的PM做的比較穩定,時間慢慢空閑出來的時候才有機會看了。短時間內,還是要以解決實際問題為主,而不是增加自己其它方面的實力。
因此,本着解決實際問題的目的,LZ就研究出一種解決當下問題的方案,可能文章的標題看起來挺牛B的,其實LZ就是簡單的利用了一下分布式的思想,以及spring框架的特性,解決了當下的參數配置問題。
問題的來源
首先來說說LZ碰到的問題,其實這個問題並不大,但卻會讓你十分厭煩。相信在座的不少猿友都經歷過這樣的事情,好不容易將項目上線了,卻出現了問題,最后找來找去卻發現,原來是某一個參數寫錯了。比如數據庫的密碼,平時開發的時候用的是123456,結果線上的是NIMEIDE,再比如某webservice的地址,平時開發測試使用的是測試地址,結果上線的時候忘記改成線上地址了。當然了,像數據庫密碼寫錯這樣的錯誤還是比較少見的,但諸如此類的問題一定不少。
出現這個問題的原因主要就是因為開發環境、測試環境以及線上環境的參數配置往往是不同的,比較正規一點的公司一般都有這三個環境。每次LZ去做系統上線時,都要仔細的檢查一遍各個參數,一不小心搞錯了,還要接受運維人員的鄙視,而且這種鄙視LZ還無法反駁,因為這確實是LZ的失誤。還有一個問題就是,在集群環境下,現在的方式需要維護多個配置信息,一不小心就可能造成集群節點的行為不一致。
總的來說,常見的系統參數往往都難免有以下幾類,比如數據庫的配置信息、webservice的地址、密鑰的鹽值、消息隊列序列名、消息服務器配置信息、緩存服務器配置信息等等。
對於這種問題一般有以下幾種解決方式。
1、最低級的一種,人工檢查方式,在上線之后,一個一個的去修改參數配置,直到沒有問題為止。這是LZ在第一家小公司的時候采取的方式,因為當時沒有專門的測試,都是開發自己調試一下沒問題就上線了,所以上線以后的東西都要自己人工檢查。
2、通過構建工具,比如ant&ivy,設置相應規則,在構建時把參數信息替換掉,比如某webservice接口的地址在測試環境是http://10.100.11.11/service,在線上環境則是http://www.cnblogs.com/service。
3、將配置信息全部存到數據庫當中,這樣的話只替換數據庫信息即可。(這也是LZ今天要着重介紹的方式,已經在項目中使用)
4、將配置信息存放在某個公用應用當中,不過這就需要這個應用可以長期穩定的運行。(這是LZ曾經YY過的方式,最終還是覺得不可取,沒有實踐過)
5、等等一系列LZ還未知的更好的方式。
縱觀以上幾種方式,LZ個人覺得最好的方式就是第三種,也就是通過數據庫獲取的方式。這種方式其實用到了一點分布式的思想,就是配置信息不再是存放在本地,而是通過網絡獲取,就像緩存一樣,存放在本地的則是一般的緩存方式,而分布式緩存,則是通過網絡來獲取緩存數據。對於數據庫存放的數據來說,本身也是通過網絡獲取的,因為現在大多數的情況下,已經將應用與數據庫從物理部署上分離。況且由於LZ的項目使用了集群,這樣的方式也可以將配置信息統一管理,而不是每個集群節點都有一份配置信息。
通過這種思想,LZ想到的還有第四種方式,這與第三種方式十分相似,但是第四種需要另外搭建專門的應用,實際操作起來可行性較差,而且穩定性也不太容易保證,因此LZ最終還是把這種方式給pass掉了。
第二種方式是公司之前一直采用的方式,但是壞處就是每當要增加一個配置參數,就要通知配置管理的人員將規則修改,而配置管理的人員往往都比較忙,有的時候新參數已經上線了,規則還沒做好。這樣的話,一旦發布,如果LZ稍有遺忘,就可能造成啟動失敗。實際上,要說啟動失敗,其實還算是好的,還能及時糾正,最怕的是啟動成功,但真正運行時系統的行為會產生異常,比如把線上的消息給發到測試服務器上去了。那個時候就不是運維人員的鄙視這么簡單了,可能就是領導的“關愛”了。盡管到目前為止,LZ好像也沒有因為這事受到過領導的“關愛”,但是每次上線都要仔細的檢查一遍參數配置,實在是費眼又費神,痛苦不堪。
說到第二種方式,還有一種弊端,就是就算規則能夠被及時更新,LZ還是得一次一次的檢查配置信息。因為替換錯了的話,責任還是在LZ,最關鍵的是,由於現在項目是集群,一檢查就得四台服務器。不過四台倒還勉強能忍,如果以后搞個十台二十台的,LZ豈不是要累死?
因此總的來說,使用第三種方式已經勢在必行。在本段的最后,總結一下第三種方式的好處。
1、由於配置信息存放在數據庫當中,而本身開發庫、測試庫和線上的生產庫就是分離的,因此只要保證數據庫的配置信息沒錯,就可以保證其它的配置信息都可以正確獲取。
2、對於已經做了集群的項目來說,可以保證配置信息只有一份。
總的說來,這種方式,與集群下的緩存解決方案有着異曲同工之妙,都是通過網絡來實現統一管理。
用代碼來說明這個問題
上面只是從實際情況和思想上分析了一下這個問題,現在LZ就使用一個比較好理解的方式來再次說明一下,這個方式當然就是代碼。接下來LZ先給出一個普通的spring的配置文件,相信大部分人對此都不會陌生。
applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.springframework.org/schema/beans" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">
<context:component-scan base-package="cn.zxl.core" />
<bean id="jdbcPropertyPlaceholderConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="locations">
<array>
<value>classpath:jdbc.properties</value>
<value>classpath:security.properties</value>
</array>
</property>
</bean>
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="${driverClassName}" />
<property name="url" value="${url}" />
<property name="username" value="${username}" />
<property name="password" value="${password}" />
<property name="initialSize" value="${initialSize}" />
<property name="maxActive" value="${maxActive}" />
<property name="maxIdle" value="${maxIdle}" />
</bean>
<bean id="sessionFactory" class="org.springframework.orm.hibernate4.LocalSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="entityInterceptor" ref="entityInterceptor"/>
<property name="packagesToScan" ref="hibernateDomainPackages" />
<property name="hibernateProperties">
<value> hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect hibernate.cache.provider_class=org.hibernate.cache.internal.NoCachingRegionFactory hibernate.current_session_context_class=org.springframework.orm.hibernate4.SpringSessionContext hibernate.show_sql=true hibernate.hbm2ddl.auto=update </value>
</property>
</bean>
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="configLocation" value="classpath:mybatis-config.xml"/>
<property name="mapperLocations" value="classpath*:mybatis/*.xml" />
</bean>
<bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate">
<constructor-arg index="0" ref="sqlSessionFactory" />
</bean>
</beans>
applicationContext-security.xml
<?xml version="1.0" encoding="UTF-8"?>
<!-- - Application context containing authentication, channel - security and web URI beans. - - Only used by "filter" artifact. - -->
<b:beans xmlns="http://www.springframework.org/schema/security" xmlns:b="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.2.xsd">
<!-- 不要過濾圖片等靜態資源 -->
<http pattern="/**/*.jpg" security="none" />
<http pattern="/**/*.png" security="none" />
<http pattern="/**/*.gif" security="none" />
<http pattern="/**/*.css" security="none" />
<http pattern="/**/*.js" security="none" />
<http pattern="/login.jsp*" security="none" />
<http pattern="/webservice/**/*" security="none" />
<!-- 這個元素用來在你的應用程序中啟用基於安全的注解 -->
<global-method-security pre-post-annotations="enabled"/>
<!-- 配置頁面訪問權限 -->
<http auto-config="true" authentication-manager-ref="authenticationManager">
<intercept-url pattern="/**" access="${accessRole}" />
<form-login login-page="${loginPage}" default-target-url="${indexPage}" always-use-default-target="true" authentication-failure-handler-ref="defaultAuthenticationFailureHandler"/>
<!-- "記住我"功能,采用持久化策略(將用戶的登錄信息存放在數據庫表中) -->
<remember-me data-source-ref="dataSource"/>
<logout />
<!-- 只能登陸一次 -->
<session-management>
<concurrency-control max-sessions="1" error-if-maximum-exceeded="true" />
</session-management>
<custom-filter ref="resourceSecurityFilter" before="FILTER_SECURITY_INTERCEPTOR"/>
</http>
<b:bean id="defaultAuthenticationFailureHandler" class="cn.zxl.core.filter.DefaultAuthenticationFailureHandler">
<b:property name="loginUrl" value="${loginPage}" />
</b:bean>
<b:bean id="resourceSecurityFilter" class="cn.zxl.core.filter.ResourceSecurityFilter">
<b:property name="authenticationManager" ref="authenticationManager" />
<b:property name="accessDecisionManager" ref="resourceAccessDecisionManager" />
<b:property name="securityMetadataSource" ref="resourceSecurityMetadataSource" />
</b:bean>
<!-- 數據中查找用戶 -->
<authentication-manager alias="authenticationManager">
<authentication-provider user-service-ref="userService">
<password-encoder hash="md5">
<salt-source user-property="username"/>
</password-encoder>
</authentication-provider>
</authentication-manager>
</b:beans>
相應的,我們一般會有以下這樣的配置文件去配置上面使用${}標注起來的信息。
jdbc.properties
driverClassName=com.mysql.jdbc.Driver url=jdbc:mysql://localhost/test username=root password=123456 initialSize=10 maxActive=50 maxIdle=10
security.properties
accessRole=ROLE_USER indexPage=/index.jsp loginPage=/login.jsp
以上是LZ自己平時寫的一個示例項目,目的是完善自己的框架,在實際的項目當中,配置信息會相當之多。按照我們第三種方式的思想,現在就需要將除了dataSource這個bean相關的配置信息以外的其它配置信息都丟到數據庫里,而這個數據庫正是當下所使用的dataSource。首先可以預見的是,我們需要對PropertyPlaceholderConfigurer做一些手腳來達到我們的目的。
解決問題第一招,使用以前的輪子
有了這個問題,LZ就要想辦法解決,雖說按照目前的方式也可以勉強使用,但LZ始終覺得這不是正道。一開始LZ的做法很簡單,就是各種百度和google,期待有其他人也遇到過這種問題,並給出一個很好的解決方案。這樣的話,不但方便,不用自己費心思找了,而且穩定性也有保證,畢竟能搜索出來就說明已經有人使用過了。現在作為PM,和以前做程序猿不太一樣,LZ需要首先保證系統的穩定性,不到萬不得以,一般不會采取沒有實踐過的方案。如果還是做程序猿那會,LZ一定會自己研究一番,然后屁顛屁顛的跑去給PM匯報自己的成果,讓他來決定是否要采用,如果成功,那皆大歡喜,說不定PM會對LZ刮目相看,如果失敗,領導也找不到LZ的頭上。
不過事實往往是殘酷的,事情沒有這么簡單,LZ在網絡上並沒有找到相關的內容,或許是LZ搜索的關鍵字還是不夠犀利。但沒辦法,找不到就是找不到,既然沒有現成的輪子,LZ就嘗試自己造一個試試。
解決問題第二招,自己造輪子
想要自己造輪子,首先要做的就是研究清楚spring在設置配置參數時做了什么,答案自然就在PropertyPlaceholderConfigurer這個類的源碼當中。
於是LZ花費了將近半個小時去研究這個類的源碼,終於搞清楚了這個類到底做了什么,結果是它主要做了兩件事。
1、讀取某一個地方的配置信息,到底讀取哪里的配置信息,由方法mergeProperties決定。
2、在bean實例獲取之前,逐個替換${}形式的參數。
如此一來問題就好辦了,我們要寫一個類去覆蓋PropertyPlaceholderConfigurer的mergeProperties方法,而這個方法當中要做的,則是從數據庫當中讀取一些配置信息。這個類的樣子最終如下所示。
package cn.zxl.core.spring; import java.io.IOException; import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.Properties; import javax.sql.DataSource; import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; /** * 從數據庫讀取配置信息 * @author zuoxiaolong * */
public class DatabaseConfigurer extends PropertyPlaceholderConfigurer implements ApplicationContextAware{ private ApplicationContext applicationContext; private String dataSourceBeanName = "dataSource"; private String querySql = "select * from database_configurer_properties"; public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } public void setDataSourceBeanName(String dataSourceBeanName) { this.dataSourceBeanName = dataSourceBeanName; } public void setQuerySql(String querySql) { this.querySql = querySql; } protected Properties mergeProperties() throws IOException { Properties properties = new Properties(); //獲取數據源
DataSource dataSource = (DataSource) applicationContext.getBean(dataSourceBeanName); Connection connection = null; try { connection = dataSource.getConnection(); Statement statement = connection.createStatement(); ResultSet resultSet = statement.executeQuery(querySql); while (resultSet.next()) { String key = resultSet.getString(1); String value = resultSet.getString(2); //存放獲取到的配置信息
properties.put(key, value); } resultSet.close(); statement.close(); connection.close(); } catch (SQLException e) { throw new IOException("load database properties failed."); } //返回供后續使用
return properties; } }
這個類的代碼並不復雜,LZ為了方便使用,給這個類設置了兩個屬性,一個是數據源的bean名稱,一個是查詢的sql。必要的時候,可以由使用者自行定制。細心的猿友可能會發現,LZ在這里構造了一個空的properties對象,而不是使用在父方法super.mergeProperties()的基礎上進行數據庫的配置信息讀取,這其實是有原因的,也是實現從數據庫讀取配置信息的關鍵。
剛才LZ已經分析過,PropertyPlaceholderConfigurer主要做了兩件事,而在mergeProperties()方法當中,只是讀取了配置信息,並沒有對bean定義當中的${}占位符進行處理。因此我們要想從數據庫讀取配置信息,必須配置兩個Configurer,而且這兩個Configurer要有順序之分。
第一個Configurer的作用則是從jdbc.properties文件當中讀取到數據庫的配置信息,並且將數據庫配置信息替換到bean定義當中。第二個Configurer則是我們的數據庫Configurer,它的作用則是從已經配置好的dataSource當中讀取其它的配置信息,從而進行后續的bean定義替換。
原本在spring的配置文件中有下面這一段。
<bean id="jdbcPropertyPlaceholderConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="locations">
<array>
<value>classpath:jdbc.properties</value>
<value>classpath:security.properties</value>
</array>
</property>
</bean>
現在經過我們的優化,我們需要改成以下形式。
<bean id="jdbcPropertyPlaceholderConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="order" value="1"/>
<property name="ignoreUnresolvablePlaceholders" value="true"/>
<property name="ignoreResourceNotFound" value="true"/>
<property name="locations">
<array>
<value>classpath:jdbc.properties</value>
</array>
</property>
</bean>
<bean id="databaseConfigurer" class="cn.zxl.core.spring.DatabaseConfigurer">
<property name="order" value="2"/>
</bean>
可以注意到,我們加入了幾個新的屬性。比如order、ignoreUnresolvablePlaceholders、ignoreResourceNotFound。order屬性的作用是保證兩個Configurer能夠按照我們想要的順序進行處理,ignoreUnresolvablePlaceholders的作用則是為了保證在jdbcPropertyPlaceholderConfigurer進行處理的時候,不至於因為未處理的占位符拋出異常。最后一個屬性ignoreResourceNotFound則是為了保證dataSource也可以由其它方式提供,比如JNDI的方式。
現在好了,你只要在你的數據庫當中創建如下這樣的表(LZ的是MQSQL數據庫,因此以下SQL只保證適用於MQSQL)。
CREATE TABLE database_configurer_properties ( key varchar(200) NOT NULL, value text, PRIMARY KEY (key) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
然后將security.properties當中的配置信息按照key/value的方式插入到這個表當中即可,當然,如果有其它的配置信息,也可以照做。這下皆大歡喜了,媽媽再也不用擔心我們把配置信息搞錯了。
小結
本文算不上是什么高端技術,只是一個小技巧,如果各位猿友能用的上的話,就給推薦下吧。