基於Struts2+Spring+iBatis的web應用最佳實踐系列之一(自動配置篇)

由於最近有點時間,便想動手寫點東西,其一算是對自己這段時間來項目經驗的一個總結,其二也希望能和大家探討下最佳實踐這個主題。說來也怪,網絡上關於這三個框架的介紹很多,整合的教程也很多,但絕大多數都屬於入門級別,淺嘗則止,對於探討如何在實際項目中用好他們,如何發揮出整合後的巨大威力的文章卻很少,甚至連javaEye都沒有最佳實踐這個分類。不知道是大家的忽視還是那些大牛們藏着掖着不願拿出來與大家分享。但筆者覺得,再好的框架給你,你不能用好它也是白搭。同時筆者也覺得,真正好的東西不是閉門造車造出來的,而是在與外界交流中不斷完善起來的。本着“不是原創的不發,沒有新意的不發”的原則,筆者在此保證,雖然有些工具和類庫等從網上搜集或改造而來,但本系列文章絕對原創,都是筆者在項目實踐當中思考總結而來。並會在本系列最後放出一個demo以及所有源代碼供下載。但同時在這裏也指出,本系列不是入門教程,需要讀者有一定的基礎,如果對有些技術點不是很明白的話,請自行查閱相關資料(網上入門級別的介紹資料很多)。

 

作爲本系列的開篇,主要討論如何合理的管理配置文件,把這個主題放在第一位主要也考慮到本篇所介紹或涉及到的技巧、工具等其後的篇章會應用到。其實寫好技術類文章也是挺難的,在實際工作中可能一個項目裏就涉及到多個新的技術點,而這些技術點往往是作爲一個互相聯繫的整體而存在的,要把涉及到的每個技術點清晰而又有條理的向讀者介紹清楚確實是一大考驗。閒話少敘,就此轉入正題。如果諸位有一定的項目經歷的話,一定會和形形色色的諸多配置打過交道,比如web應用當中的web.xml,struts2中的struts.xml,又比如iBatis中對數據源的配置。但不知道大家有沒有發現,其中有些配置是部署特有的(deployment specific),比如iBatis中對數據源的配置;而有些是應用特有的(application specific),比如struts2中對攔截器的配置。對那些應用特有的配置,不管這個應用被部署到那裏,這個配置都不會發生變化,而部署特有的配置則不一樣,部署到不同的環境具體的配置則不一樣。最典型的莫過於對數據源的配置,開發人員在開發的時候需要將應用程序連接到本地的數據庫做開發用,當部署到生產環境時則需要切換到生產環境的數據庫。如果不能將部署特有的配置與應用特有的配置相隔離,那麼該應用的部署則會成爲噩夢。

 

好在我們有Spring。那麼,Spring將如何幫助我們把部署特有的配置同應用特有的配置隔離出來?Spring就象一個寶藏,只要細心發掘,總會有意向不到的收穫。在Spring中有一個PropertyPlaceholderConfigurer類,使用這個類在解析bean配置中遇到${...}這樣的佔位符後可以從properties文件中將屬性讀取出來替換佔位符。

 

Example XML context definition:

<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
   <property name="driverClassName"><value>${driver}</value></property>
   <property name="url"><value>jdbc:${dbname}</value></property>
</bean>

 

Example properties file:

driver=com.mysql.jdbc.Driver
dbname=mysql:mydb

 

當然 PropertyPlaceholderConfigurer本身也要配置成一個bean,在這個bean中用以指定我們properties文件的位置。

 

<bean id="propertyConfigurer"
	class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
	<property name="locations">
		<list>
			<value>classpath:com/meidusa/demo/dal/datasource.properties</value>
		</list>
	</property>
</bean>

 

這樣我們通過使用PropertyPlaceholderConfigurer將部署特有的配置隔離到了一個properties文件裏,有意思的是PropertyPlaceholderConfigurer還有一個孿生兄弟PropertyOverrideConfigurer,同樣也是從properties文件中讀取配置屬性,但與PropertyPlaceholderConfigurer不同的是,使用PropertyOverrideConfigurer不需要再使用佔位符,而是可以在bean定義中先指定一個默認值,而在properties文件中指定的值會override bean的默認值,形象的說就是PropertyOverrideConfigurer是將值從properties文件中push到bean定義中而PropertyPlaceholderConfigurer則是將值從properties文件中pull到bean定義中。

 

諸位看到這裏或許會想,這篇文章裏到目前爲止介紹東西的基本上也是屬於大路貨性質,網上基本上都有。是的,這點我承認,不過寫文章總得有個過渡是吧,一上來就把所有的貨都倒出來也不見得就討人喜歡。不過我保證,如果大家耐着性子看下去一定會有所收穫的。

 

題外話點到爲止,我們繼續。在Spring中普遍使用的resourceLoader一般有三種,即classpath,file和url。在我們上面的那個例子中我們就使用了classpath resourceLoader。但是作爲部署特有的配置不論使用這三種resourceLoader中的任何一種筆者都覺得不是很合適。顯然,我們不想使用classpath resourceLoader,因爲我們並不想將部署特有的配置打包到應用裏,我們也不想使用file resourceLoader,因爲在不同的部署環境中,properties文件的絕對路徑並不一定相同,所以指定一個絕對路徑並不是一個很好的做法。那使用url resourceLoader就更不靠譜了。既然這樣,那我們還有沒有更好的辦法呢?首先,思路是這樣的,一般而言,一個典型的部署過程是將開發人員在windows平臺上開發好的應用打包然後部署到linux/Unix平臺上(也有可能仍然是windows),但是不管是windows平臺還是Linux或者Unix平臺,都有user home目錄。在windows平臺上是C:\Documents and Settings\user目錄下,Linux或者Unix則是在/home/user目錄下,既然如此,那我們是否可以把properties文件放在user home目錄下,把部署特有的配置從應用當中隔離出來,使得應用在部署的時候不需要做額外的配置工作。思路是這樣的,那麼具體如何做到呢?首先我們看一下下面這張類繼承關係圖,從圖上我們可以看到PropertyPlaceholderConfigurer繼承自頂層的PropertiesLoaderSupport

 

 

這個類裏有一個setLocations方法,接受一個Resource類型的數組作爲參數,對比上面PropertyPlaceholderConfigurer的bean配置我們就可以發現locations標籤下值都會被解析成Resource,而這個resource本身則包含了訪問這個resource的方法,在這裏resource代表的則是properties文件。

	/**
	 * Set locations of properties files to be loaded.
	 * <p>Can point to classic properties files or to XML files
	 * that follow JDK 1.5's properties XML format.
	 * <p>Note: Properties defined in later files will override
	 * properties defined earlier files, in case of overlapping keys.
	 * Hence, make sure that the most specific files are the last
	 * ones in the given list of locations.
	 */
	public void setLocations(Resource[] locations) {
		this.locations = locations;
	}

 

而這個解析過程應該是由applicationContext完成的。研究一下代碼發現,所有的applicationContext都間接的繼承或實現了ResourceLoader這個接口,這個接口主要有一個getResource方法,而Resource本身也是一個接口。

 

Resource Loader接口

public interface ResourceLoader {

	Resource getResource(String location);

	ClassLoader getClassLoader();
}

 

Resource接口

public interface Resource extends InputStreamSource {

	boolean exists();

	boolean isReadable();

	boolean isOpen();

	URI getURI() throws IOException;

	File getFile() throws IOException;

	Resource createRelative(String relativePath) throws IOException;

	String getDescr*ption();
}

 

 
研究到這一步我們就受到一個啓發,既然applicationContext通過解析後的Resource來訪問這個properties文件的話,那我們是否可以通過寫一個bean,這個bean實現Resource這個接口,並且由這個bean負責解析對這個properties文件訪問呢?既然這個bean是一個Resource的話,那麼它就可以被applicationContext裝載。

 

 假設我們的properties文件名是projectName.properties,並且這個bean接受一個名稱是projectName的屬性,那我們的PropertyPlaceholderConfigurer可以這樣配置

<beans default-autowire="byName">
	<bean id="propertyConfigurer"
		class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
		<property name="locations">
			<list>
				<bean class="com.meidusa.toolkit.common.spring.DefinedFileSystemResource">
					<property name="projectName">
						<value>demo</value>
					</property>
				</bean>
			</list>
		</property>
	</bean>
</beans>

 

 com.meidusa.toolkit.common.spring.DefinedFileSystemResource是這個bean的class。這個bean通過projectName的值來解析具體的properties文件的位置。

 

而projectName的值是應用特有的配置而不是部署特有的,通過由這個bean來解析具體的properties文件的位置,我們成功的實現了對部署特有的配置與應用特有的配置的分離。

 

因爲這個類的定義很長,所以這裏只放出部分代碼,從下面的代碼中可以看出,這個類繼承了AbstractResource並實現了InitializingBean這個接口。AbstractResource是對Resource這個接口的默認實現,而InitializingBean則定義了這個類裏最重要的一個方法,即afterPropertiesSet(),這個方法在projectName屬性設置後被調用,正是由這個方法計算出對properties文件的訪問路徑。

 

public class DefinedFileSystemResource extends AbstractResource implements
		InitializingBean {
	private static Logger logger = Logger.getLogger(DefinedFileSystemResource.class);
	private String projectName;
	private File file;
	private String path;

	public DefinedFileSystemResource() {

	}

	public void afterPropertiesSet() throws Exception {
		try{
	
			this.path = System.getProperty("user.home");
			this.path = StringUtils.cleanPath(path);
			this.file = new File(path,projectName+".properties");
		}finally{
			logger.info("loading project config file from :"+file.getAbsolutePath());
		}
	}

}

 

按說到這裏應該算是圓滿了,但筆者還是不得不指出,使用這種方法還是有一個致命的不足之處,那就是對Spring的依賴。雖說Spring很好很強大,對幾乎所有的主流框架都有一定的支持,但總有不支持的吧?特別是自己寫的工具類庫,比如在這個系列的下一篇我們會討論到一個自己定製的Struts2的cookie攔截器,這個攔截器會用到一個自己定義的xml配置文件,這個配置文件裏會放置一些部署特有的配置信息,遇到這種情況Spring就沒輒了。那麼,我們有沒有一種更普遍適用的解決方案呢?

 

 答案是肯定的,這也涉及到我們這篇文章的主題,即自動化配置。在討論這個新的方法之前,有必要先引入一下maven這個管理工具。如果諸位對maven不是很瞭解的話可以去Juven的博客瀏覽一番,在他的博客裏有着對maven十分詳盡的介紹,地址是http://juvenshun.iteye.com/  不過maven並不是我們今天的主題,在這裏我主要想介紹一個maven的插件,maven-autoconfig-plugin。

 

在命令行界面下運行maven命令可以得到如下輸出

 

mvn help:describe -Dplugin=autoconfig -Ddetail

 

Name: maven-autoconfig-plugin
Descr*ption: (no descr*ption available)
Group Id: com.meidusa.toolkit.plugin
Artifact Id: maven-autoconfig-plugin
Version: 1.0
Goal Prefix: autoconfig

This plugin has 1 goal:

autoconfig:config
  Descr*ption: (no descr*ption available)
  Deprecated. No reason given
  Implementation: com.meidusa.toolkit.plugin.autoconfig.AutoConfig
  Language: java

  Available parameters:

    charset
      (no descr*ption available)
      Deprecated. No reason given

    excludeDescr*ptors
      excludeDescr*ptors
      Deprecated. No reason given

    excludePackages
      excludePackages
      Deprecated. No reason given

    includeDescr*ptors
      includeDescr*ptors
      Deprecated. No reason given

    includePackages
      includePackages
      Deprecated. No reason given

    projectName
      projectName
      Deprecated. No reason given

 

從上面的輸出中我們可以看到這個autoconfig插件有且只有一個goal,那就是config。另外支持的參數有charset,excludeDescr*ptors,excludePackages,includeDescr*ptors,includePackages,projectName。

 

一個典型的應用是在父項目的pom中將這個插件的config goal bind到initialize這個phase中,也就是說在maven構建的初始階段即運行autoconfig這個插件,如果是第一次運行,它會根據includeDescr*ptors參數掃描所有的自動配置符文件,通過運行一個嚮導配置好user home目錄下的projectName.properties屬性文件。若projectName.properties屬性文件已經存在並已配置好的時候,這個插件會根據projectName的值自動掃描這個文件並讀取裏面的值,同時includeDescr*ptors參數包含的自動配置描述符文件中若有<script>項,插件會根據讀取的屬性渲染定義在配置描述符裏的模板生成真正的配置文件,即完成了配置的自動化。在子項目中則把這個插件的config goal bind到install phase中,這樣在打包的時候就可以將自動配置好的配置文件打包進war中。

 

在父項目pom.xml中的配置片段如下

 

<plugin>
	<groupId>com.meidusa.toolkit.plugin</groupId>
	<artifactId>maven-autoconfig-plugin</artifactId>
	<configuration>
		<projectName>demo</projectName>
		<includeDescr*ptors>deploy/**/auto-config.xml</includeDescr*ptors>
	</configuration>
	<executions>
		<execution>
			<phase>initialize</phase>
			<goals>
				<goal>config</goal>
			</goals>
		</execution>
	</executions>
</plugin>
<plugin>

 

在子項目pom.xml中配置片段如下

 

<plugin>
	<groupId>com.meidusa.toolkit.plugin</groupId>
	<artifactId>maven-autoconfig-plugin</artifactId>
	<configuration>
		<projectName>demo</projectName>
		<includePackages>target/*.war</includePackages>
		<includeDescriptors>src/**/auto-config.xml</includeDescriptors>
	</configuration>
	<executions>
		<execution>
			<phase>install</phase>
			<goals>
				<goal>config</goal>
			</goals>
		</execution>
	</executions>
</plugin>

   

groupId和artifactId標識了這個插件的座標,configuration標籤則指定了運行這個插件時給的參數及其值,executions標籤及其子標籤則表明如何將這個插件綁定到使用maven構建的生命週期中。

對於maven的配置文件pom.xml及生命週期以及插件的goal等概念讀者請自行查詢相關資料,這裏不再贅述。

 

 autoconfig的描述符本身也是一個xml配置文件,group定義了properties屬性,script則定義了需要渲染的模板

 

<?xml version="1.0" encoding="utf-8"?>
<config>
    <group name="usercookie setting" description="用戶cookie的配置">
    	<property name="cookie.encryptKey" defaultValue="ei*736TR" description="cookie加密的key,至少8位" />
    	<property name="cookie.loginUrl" defaultValue="http://demo.meidusa.com:8080" description="cookie失敗後登錄的地址" />
    	<property name="cookie.algorithm" defaultValue="DES" description="cookie加密的算法" />
    	<property name="cookie.domain" defaultValue="demo.meidusa.com" description="cookie domain" />
    </group>
    <script>
        <generate template="cookieMapping.xml" destfile="deploy/conf/cookieMapping.xml" charset="utf-8"/>
    </script>
</config>

 

 

 cookie配置文件的模板,可以看到需要被渲染替換的屬性佔位符。

 

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE cookieMapping SYSTEM "cookieMapping.dtd">
<cookieMapping
	cookieClass="com.meidusa.pirateweb.web.account.Cookie"
	encryptKey="${cookie_encryptKey}"
	loginUrl="${cookie_loginUrl}" algorithm="${cookie_algorithm}" maxLifeTime="1440" maxIdleTime="-1">
	
	<cookie cookieName="meidusa_cookie2" innerCookieName="loginId">
		<property name="age">1000</property>
		<property name="domain">${cookie_domain}</property>
		<property name="writable">true</property>
		<property name="secure">true</property>
		<property name="path">/</property>
	</cookie>

	<cookie cookieName="meidusa_mask" innerCookieName="mask">
		<property name="age">1000</property>
		<property name="domain">${cookie_domain}</property>
		<property name="writable">true</property>
		<property name="secure">true</property>
		<property name="path">/</property>
	</cookie>
</cookieMapping>

 

 

假設projectName是demo,在user home目錄下demo.properties文件的內容可以是

 

cookie.algorithm   = DES
cookie.domain      = www.meidusa.com
cookie.encryptKey  = ei*736TR
cookie.loginUrl    = http://www.meidusa.com:8080/account/signin.html

 

 最終渲染出來的配置文件是

 

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE cookieMapping SYSTEM "cookieMapping.dtd">
<cookieMapping
	cookieClass="com.meidusa.pirateweb.web.account.Cookie"
	encryptKey="ei*736TR"
	loginUrl="http://www.meidusa.com:8080/account/signin.html" algorithm="DES" maxLifeTime="1440" maxIdleTime="-1">
	
	<cookie cookieName="meidusa_cookie2" innerCookieName="loginId">
		<property name="age">1000</property>
		<property name="domain">www.meidusa.com</property>
		<property name="writable">true</property>
		<property name="secure">true</property>
		<property name="path">/</property>
	</cookie>

	<cookie cookieName="meidusa_mask" innerCookieName="mask">
		<property name="age">1000</property>
		<property name="domain">www.meidusa.com</property>
		<property name="writable">true</property>
		<property name="secure">true</property>
		<property name="path">/</property>
	</cookie>
</cookieMapping>

 

 這裏我們先不管這個cookie配置文件是作什麼用的(本系列下一篇會談到),但是我們可以看到域名,登錄的url等部署特有的被自動配置了。

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章