從數據庫中獲取spring配置參數

     教你如何利用分佈式的思想處理集羣的參數配置信息spring的configurer妙用(http://www.it165.net/admin/html/201404/2902.html)

作者:左瀟龍  來源:IT165收集  發佈日期:2014-04-28 22:33:56
  • 原文地址: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"?>
    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. 
    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 implementsApplicationContextAware{
    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的方式插入到這個表當中即可,當然,如果有其它的配置信息,也可以照做。這下皆大歡喜了,媽媽再也不用擔心我們把配置信息搞錯了。

    小結

      本文算不上是什麼高端技術,只是一個小技巧,如果各位猿友能用的上的話,就給推薦下吧。


這個人的也不錯哦:http://blog.csdn.net/maoxiang/article/details/4829553
發佈了37 篇原創文章 · 獲贊 0 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章