-
原文地址:http://www.it165.net/admin/html/201404/2902.html
-
引言
最近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
01.
<?xml version=
"1.0"
encoding=
"UTF-8"
?>
02.
<beans xmlns:xsi=
"http://www.w3.org/2001/XMLSchema-instance"
03.
xmlns=
"http://www.springframework.org/schema/beans"
04.
xmlns:tx=
"http://www.springframework.org/schema/tx"
05.
xmlns:context=
"http://www.springframework.org/schema/context"
06.
xmlns:aop=
"http://www.springframework.org/schema/aop"
07.
xsi:schemaLocation="
08.
http:
//www.springframework.org/schema/beans
09.
http:
//www.springframework.org/schema/beans/spring-beans-4.0.xsd
10.
http:
//www.springframework.org/schema/tx
11.
http:
//www.springframework.org/schema/tx/spring-tx-4.0.xsd
12.
http:
//www.springframework.org/schema/aop
13.
http:
//www.springframework.org/schema/aop/spring-aop-4.0.xsd
14.
http:
//www.springframework.org/schema/context
15.
http:
//www.springframework.org/schema/context/spring-context-4.0.xsd">
16.
17.
<context:component-scan base-
package
=
"cn.zxl.core"
/>
18.
19.
<bean id=
"jdbcPropertyPlaceholderConfigurer"
class
=
"org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"
>
20.
<property name=
"locations"
>
21.
<array>
22.
<value>classpath:jdbc.properties</value><br> <value>classpath:security.properties</value>
23.
</array>
24.
</property>
25.
</bean>
26.
27.
<bean id=
"dataSource"
class
=
"org.apache.commons.dbcp.BasicDataSource"
destroy-method=
"close"
>
28.
<property name=
"driverClassName"
value=
"${driverClassName}"
/>
29.
<property name=
"url"
value=
"${url}"
/>
30.
<property name=
"username"
value=
"${username}"
/>
31.
<property name=
"password"
value=
"${password}"
/>
32.
<property name=
"initialSize"
value=
"${initialSize}"
/>
33.
<property name=
"maxActive"
value=
"${maxActive}"
/>
34.
<property name=
"maxIdle"
value=
"${maxIdle}"
/>
35.
</bean>
36.
37.
<bean id=
"sessionFactory"
class
=
"org.springframework.orm.hibernate4.LocalSessionFactoryBean"
>
38.
<property name=
"dataSource"
ref=
"dataSource"
/>
39.
<property name=
"entityInterceptor"
ref=
"entityInterceptor"
/>
40.
<property name=
"packagesToScan"
ref=
"hibernateDomainPackages"
/>
41.
<property name=
"hibernateProperties"
>
42.
<value>
43.
hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
44.
hibernate.cache.provider_class=org.hibernate.cache.internal.NoCachingRegionFactory
45.
hibernate.current_session_context_class=org.springframework.orm.hibernate4.SpringSessionContext
46.
hibernate.show_sql=
true
47.
hibernate.hbm2ddl.auto=update
48.
</value>
49.
</property>
50.
</bean>
51.
52.
<bean id=
"sqlSessionFactory"
class
=
"org.mybatis.spring.SqlSessionFactoryBean"
>
53.
<property name=
"dataSource"
ref=
"dataSource"
/>
54.
<property name=
"configLocation"
value=
"classpath:mybatis-config.xml"
/>
55.
<property name=
"mapperLocations"
value=
"classpath*:mybatis/*.xml"
/>
56.
</bean>
57.
58.
<bean id=
"sqlSessionTemplate"
class
=
"org.mybatis.spring.SqlSessionTemplate"
>
59.
<constructor-arg index=
"0"
ref=
"sqlSessionFactory"
/>
60.
</bean>
61.
62.
</beans>
applicationContext-security.xml
01.
<?xml version=
"1.0"
encoding=
"UTF-8"
?>
02.
<!-- - Application context containing authentication, channel - security
03.
and web URI beans. - - Only used by
"filter"
artifact. - -->
04.
05.
<b:beans xmlns=
"http://www.springframework.org/schema/security"
06.
xmlns:b=
"http://www.springframework.org/schema/beans"
xmlns:xsi=
"http://www.w3.org/2001/XMLSchema-instance"
07.
xmlns:p=
"http://www.springframework.org/schema/p"
08.
xsi:schemaLocation="http:
//www.springframework.org/schema/beans
09.
http:
//www.springframework.org/schema/beans/spring-beans-4.0.xsd
10.
http:
//www.springframework.org/schema/security
11.
http:
//www.springframework.org/schema/security/spring-security-3.2.xsd">
12.
13.
<!-- 不要過濾圖片等靜態資源 -->
14.
<http pattern=
"/**/*.jpg"
security=
"none"
/>
15.
<http pattern=
"/**/*.png"
security=
"none"
/>
16.
<http pattern=
"/**/*.gif"
security=
"none"
/>
17.
<http pattern=
"/**/*.css"
security=
"none"
/>
18.
<http pattern=
"/**/*.js"
security=
"none"
/>
19.
<http pattern=
"/login.<a href="
http:
//www.it165.net/pro/webjsp/" target="_blank" class="keylink">jsp</a>*" security="none" />
20.
<http pattern=
"/webservice/**/*"
security=
"none"
/>
21.
22.
<!-- 這個元素用來在你的應用程序中啓用基於安全的註解 -->
23.
<global-method-security pre-post-annotations=
"enabled"
/>
24.
25.
<!-- 配置頁面訪問權限 -->
26.
<http auto-config=
"true"
authentication-manager-ref=
"authenticationManager"
>
27.
28.
<intercept-url pattern=
"/**"
access=
"${accessRole}"
/>
29.
30.
<form-login login-page=
"${loginPage}"
default
-target-url=
"${indexPage}"
31.
always-use-
default
-target=
"true"
authentication-failure-handler-ref=
"defaultAuthenticationFailureHandler"
/>
32.
33.
<!--
"記住我"
功能,採用持久化策略(將用戶的登錄信息存放在數據庫表中) -->
34.
<remember-me data-source-ref=
"dataSource"
/>
35.
36.
<logout />
37.
38.
<!-- 只能登陸一次 -->
39.
<session-management>
40.
<concurrency-control max-sessions=
"1"
error-
if
-maximum-exceeded=
"true"
/>
41.
</session-management>
42.
43.
<custom-filter ref=
"resourceSecurityFilter"
before=
"FILTER_SECURITY_INTERCEPTOR"
/>
44.
</http>
45.
46.
<b:bean id=
"defaultAuthenticationFailureHandler"
class
=
"cn.zxl.core.filter.DefaultAuthenticationFailureHandler"
>
47.
<b:property name=
"loginUrl"
value=
"${loginPage}"
/>
48.
</b:bean>
49.
50.
<b:bean id=
"resourceSecurityFilter"
class
=
"cn.zxl.core.filter.ResourceSecurityFilter"
>
51.
<b:property name=
"authenticationManager"
ref=
"authenticationManager"
/>
52.
<b:property name=
"accessDecisionManager"
ref=
"resourceAccessDecisionManager"
/>
53.
<b:property name=
"securityMetadataSource"
ref=
"resourceSecurityMetadataSource"
/>
54.
</b:bean>
55.
56.
<!-- 數據中查找用戶 -->
57.
<authentication-manager alias=
"authenticationManager"
>
58.
<authentication-provider user-service-ref=
"userService"
>
59.
<password-encoder hash=
"md5"
>
60.
<salt-source user-property=
"username"
/>
61.
</password-encoder>
62.
</authentication-provider>
63.
</authentication-manager>
64.
65.
</b:beans>
相應的,我們一般會有以下這樣的配置文件去配置上面使用${}標註起來的信息。
jdbc.properties
1.
driverClassName=com.mysql.jdbc.Driver
2.
url=jdbc:mysql:
//localhost/test
3.
username=root
4.
password=
123456
5.
initialSize=
10
6.
maxActive=
50
7.
maxIdle=
10
security.properties
1.
accessRole=ROLE_USER
2.
indexPage=/index.<a href=
"http://www.it165.net/pro/webjsp/"
target=
"_blank"
class
=
"keylink"
>jsp</a>
3.
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方法,而這個方法當中要做的,則是從數據庫當中讀取一些配置信息。這個類的樣子最終如下所示。
01.
package
cn.zxl.core.spring;
02.
03.
import
java.io.IOException;
04.
import
java.sql.Connection;
05.
import
java.sql.ResultSet;
06.
import
java.sql.SQLException;
07.
import
java.sql.Statement;
08.
import
java.util.Properties;
09.
10.
import
javax.sql.DataSource;
11.
12.
import
org.springframework.beans.BeansException;
13.
import
org.springframework.beans.factory.config.PropertyPlaceholderConfigurer;
14.
import
org.springframework.context.ApplicationContext;
15.
import
org.springframework.context.ApplicationContextAware;
16.
17.
/**
18.
* 從數據庫讀取配置信息
19.
* @author zuoxiaolong
20.
*
21.
*/
22.
public
class
DatabaseConfigurer
extends
PropertyPlaceholderConfigurer
implements
ApplicationContextAware{
23.
24.
private
ApplicationContext applicationContext;
25.
26.
private
String dataSourceBeanName =
"dataSource"
;
27.
28.
private
String querySql =
"select * from database_configurer_properties"
;
29.
30.
public
void
setApplicationContext(ApplicationContext applicationContext)
31.
throws
BeansException {
32.
this
.applicationContext = applicationContext;
33.
}
34.
35.
public
void
setDataSourceBeanName(String dataSourceBeanName) {
36.
this
.dataSourceBeanName = dataSourceBeanName;
37.
}
38.
39.
public
void
setQuerySql(String querySql) {
40.
this
.querySql = querySql;
41.
}
42.
43.
protected
Properties mergeProperties()
throws
IOException {
44.
Properties properties =
new
Properties();
45.
//獲取數據源
46.
DataSource dataSource = (DataSource) applicationContext.getBean(dataSourceBeanName);
47.
Connection connection =
null
;
48.
try
{
49.
connection = dataSource.getConnection();
50.
Statement statement = connection.createStatement();
51.
ResultSet resultSet = statement.executeQuery(querySql);
52.
while
(resultSet.next()) {
53.
String key = resultSet.getString(
1
);
54.
String value = resultSet.getString(
2
);
55.
//存放獲取到的配置信息
56.
properties.put(key, value);
57.
}
58.
resultSet.close();
59.
statement.close();
60.
connection.close();
61.
}
catch
(SQLException e) {
62.
throw
new
IOException(
"load database properties failed."
);
63.
}
64.
//返回供後續使用
65.
return
properties;
66.
}
67.
68.
}
這個類的代碼並不複雜,LZ爲了方便使用,給這個類設置了兩個屬性,一個是數據源的bean名稱,一個是查詢的sql。必要的時候,可以由使用者自行定製。細心的猿友可能會發現,LZ在這裏構造了一個空的properties對象,而不是使用在父方法super.mergeProperties()的基礎上進行數據庫的配置信息讀取,這其實是有原因的,也是實現從數據庫讀取配置信息的關鍵。
剛纔LZ已經分析過,PropertyPlaceholderConfigurer主要做了兩件事,而在mergeProperties()方法當中,只是讀取了配置信息,並沒有對bean定義當中的${}佔位符進行處理。因此我們要想從數據庫讀取配置信息,必須配置兩個Configurer,而且這兩個Configurer要有順序之分。
第一個Configurer的作用則是從jdbc.properties文件當中讀取到數據庫的配置信息,並且將數據庫配置信息替換到bean定義當中。第二個Configurer則是我們的數據庫Configurer,它的作用則是從已經配置好的dataSource當中讀取其它的配置信息,從而進行後續的bean定義替換。
原本在spring的配置文件中有下面這一段。
1.
<bean id=
"jdbcPropertyPlaceholderConfigurer"
class
=
"org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"
>
2.
<property name=
"locations"
>
3.
<array>
4.
<value>classpath:jdbc.properties</value>
5.
<value>classpath:security.properties</value>
6.
</array>
7.
</property>
8.
</bean>
現在經過我們的優化,我們需要改成以下形式。
01.
<bean id=
"jdbcPropertyPlaceholderConfigurer"
class
=
"org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"
>
02.
<property name=
"order"
value=
"1"
/>
03.
<property name=
"ignoreUnresolvablePlaceholders"
value=
"true"
/>
04.
<property name=
"ignoreResourceNotFound"
value=
"true"
/>
05.
<property name=
"locations"
>
06.
<array>
07.
<value>classpath:jdbc.properties</value>
08.
</array>
09.
</property>
10.
</bean>
11.
12.
<bean id=
"databaseConfigurer"
class
=
"cn.zxl.core.spring.DatabaseConfigurer"
>
13.
<property name=
"order"
value=
"2"
/>
14.
</bean>
可以注意到,我們加入了幾個新的屬性。比如order、ignoreUnresolvablePlaceholders、ignoreResourceNotFound。order屬性的作用是保證兩個Configurer能夠按照我們想要的順序進行處理,ignoreUnresolvablePlaceholders的作用則是爲了保證在jdbcPropertyPlaceholderConfigurer進行處理的時候,不至於因爲未處理的佔位符拋出異常。最後一個屬性ignoreResourceNotFound則是爲了保證dataSource也可以由其它方式提供,比如JNDI的方式。
現在好了,你只要在你的數據庫當中創建如下這樣的表(LZ的是MQSQL數據庫,因此以下SQL只保證適用於MQSQL)。
1.
CREATE TABLE database_configurer_properties (
2.
key varchar(
200
) NOT NULL,
3.
value text,
4.
PRIMARY KEY (key)
5.
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
然後將security.properties當中的配置信息按照key/value的方式插入到這個表當中即可,當然,如果有其它的配置信息,也可以照做。這下皆大歡喜了,媽媽再也不用擔心我們把配置信息搞錯了。
小結
本文算不上是什麼高端技術,只是一個小技巧,如果各位猿友能用的上的話,就給推薦下吧。
從數據庫中獲取spring配置參數
這個人的也不錯哦:http://blog.csdn.net/maoxiang/article/details/4829553
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.