教你如何利用分布式的思想处理集群的参数配置信息spring的configurer妙用



作者:左潇龙    发布日期:2014-04-28 22:33:56



引言

  最近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,在线上环境则是。

  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 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的方式插入到这个表当中即可,当然,如果有其它的配置信息,也可以照做。这下皆大欢喜了,妈妈再也不用担心我们把配置信息搞错了。

小结

  本文算不上是什么高端技术,只是一个小技巧,如果各位猿友能用的上的话,就给推荐下吧。