Struts2+Spring+Hibernate 高效開發的最佳實踐 S2SH

引言

SSH(Struts2+Spring+Hibernate)是最爲 Java 業界熟知的 Java EE Web 組件層的開發技術。很多人提起 Java EE,甚至都會將其誤認爲就是 SSH。無論是書籍還是電子教程,大部分都已經千篇一律,講解各種標籤、配置的用法。許多人包括筆者在內,第一次使用 SSH 的時候,按照教程的介紹進行開發。繁瑣的配置,重複的修改配置,不斷定義的參數轉換器,真的讓筆者苦不堪言。本文對 SSH 的開發模式嘗試了重新定義,按照規約優於配置的原則,利用 Java 反射、註解等技術,設計了新的一套 SSH 開發框架,應用該框架,可以大大提高開發效率,筆者多次將該開發框架應用在各種小型 SSH Web 應用系統之中,屢試不爽。

閱讀文章之前,讀者需要對 SSH 的結合開發有一些瞭解,最好是有實踐的經驗,特別是對 Struts2 需要較爲了解,掌握 Struts2 自定義攔截器、自定義驗證器等的開發。另外,讀者還需要掌握一些前提技術,包括 Java 反射、Java 註解、理解事務隔離級別等。

文章首先進行框架的總體介紹,然後分點介紹各個部分的詳細設計。另外,文章還將列舉該框架中採用的技術特點及其相應目的,希望讀者可以從中獲益。

框架總體介紹

文章之所以說框架是通用的,因爲它的思想適應任何的業務需求。按照文章介紹,按照介紹的框架搭建完代碼架構後,可以屏蔽許多技術細節,讓開發人員專注於業務邏輯的實現,這些繁瑣的技術細節包括技術配置、權限控制、頁面跳轉、錯誤處理等等。框架大致的風格如圖 1 所示,總體來說,該框架遵守了“規約優於配置”的原則。


圖 1. 框架大致風格(查看大圖 
圖 1. 框架大致風格 

上圖中,系統只有一個 action 配置。每個業務操作,不再對應一個 ActionSupport 子類,而是對應一個 ActionSupport 子類的類方法,利用 Struts2 的動態方法特性,使得業務方法擺脫了繁瑣的配置,方便的增加和刪除。全面的 result 配置,解決了各種頁面跳轉的問題。Action 方法的起名,遵守了權限與業務模型規約,讓模型選擇與權限控制交給框架來實現,同時訪問的操作名就是【方法名】 .action。下面,將一一對框架的各部分進行講解。

Struts2 的不動配置

該框架中,我們建議只定義少量的類繼承於 ActionSupport,這樣可以使得 struts.xml 的配置儘量減少,甚至只進行少量配置,而不用隨着業務的增加而修改配置。清單 1 是筆者爲一個教師考勤系統定義的 Action 配置。


清單 1. Struts2 的配置清單
				 
 <action name="*" method="{1}" class="Main"> 
     <interceptor-ref name="fileUpload"> 
         <param name="maximumSize">4073741824</param> 
     </interceptor-ref> 
     <interceptor-ref name="myInterceptorStack"></interceptor-ref> 
     <result name="input">/noDir/error.jsp</result> 
     <result type="json" name="success"></result> 
     <result name="errorJson" type="json"></result> 
     <result type="json" name="error"></result> 
     <result type="stream" name="stream"> 
         <param name="contentType">${contentType}</param> 
         <param name="contentDisposition">fileName="${inputFileName}"</param> 
         <param name="inputName">inputStream</param>  
     </result> 
     <result name="dynamic">/${url}</result> 
     <result name="otherAction" type="redirectAction">/${url}</result> 
     <result name="red" type="redirect">/${url}</result> 
 </action> 

清單 1 定義了很多規則。首先,該配置使用了動態方法調用技術,這可以使得許多的 Action 方法可以聲明到一個類裏,不用重複定義 Action 類,同時對業務的增加和刪除可以簡約到對 Action 類裏方法增加和刪除,增加的 Action 方法不需進行其他配置,如果業務被刪除,則只需要將方法註釋或者刪除,非常方便。第二,配置的 package 繼承於 json-default,也就是說這個包裏的 action 是支持 Ajax 調用的,默認的,如果返回 ERROR、SUCCESS,則會將 Action 序列化爲 JSON 返回到客戶端。我們定義了各種的跳轉類型,包括重定向到頁面(具體的頁面由 Action 裏的 url 指定)、重定向到 Action、重定向到錯誤頁面、流類型的返回等等,這爲我們動態的選擇返回結果數據提供了方便。那麼使用上述的配置,對我們開發有什麼好處呢?假設有一個新的業務,我們只需要在 Action 添加新的方法,如清單 2 所示。


清單 2. 添加新業務方法
				 
public String business() throws Exception
	{
		…business process…

		if (has error)
		{
			addFieldError(“error message”) return INPUT;
		} else {
			//url是action中定義的一個String變量,指定跳轉地址
			url = “business.jsp”; return “dynamic”;
		} 
	}


調用這個新的業務方法,只需要調用這個鏈接:http://{host}:{port}/{webapp}/business.action。正如我們看到的,定義了新的業務邏輯方法,我們沒有修改或添加任何配置,因爲我們的配置是完整的,考慮到了各種跳轉、錯誤情況,同時動態方法調用特性,讓我們可以動態的指定業務方法。如果我們對清單 2 中的跳轉代碼進行抽取,清單 2 會更加簡單。如清單 3 所示。


清單 3. 抽取基礎方法後的業務方法
				 
public String business() throws Exception
{
		…business process…

		if (has error)
		{
			return redirectToErrorPage("error message")
		} else {
			return redirectToPage("business.jsp");
		}
}


上面的清單中,我們抽取了 redirectToErrorPage 和 redirectToPage 方法,這樣就可以使得其他的業務方法可以重用這些跳轉方法,整個業務過程變得清晰易懂。類似的我們還可以抽取出 redirectToAction(跳轉到另一個業務方法)、redirectStream(流類型的跳轉)、redirectToAnotherPage(用於重定向的跳轉)、redirectToJson(Ajax 的跳轉)等等。這樣這些公共方法就可以讓其他的開發人員一起使用。程序員可以從跳轉、錯誤提示、重複配置 Action 的痛苦中解救出來,專注於編寫業務邏輯。

ModelDriven 的規約

有了上面的配置,我們還不能做到完全脫離配置。比如,我們定義了一個 Action 類繼承於 ActionSupport,我們知道使用 ModelDriven 可以將用戶上傳的數據封裝到一個業務 Bean 裏,而不用直接在 Action 裏聲明變量,這很重要。我相信有很多讀者遇到過這個問題。當業務 Bean 不同時,也就是需要用戶上傳的數據不同時,我們就要隨之添加新的 Action,這直接導致修改 struts.xml 的配置,然後還要修改 applicationContext.xml 的事務配置、Bean 的配置,哪天我們不要這個業務了,又要重複的修改刪除配置,筆者開始時爲此事近乎抓狂,爲什麼我們不能只定義一個 Action,而 ModelDriven 的模型動態改變呢?

仔細考慮 Struts2 的機制,Struts2 將客戶端的參數裝配到 ModelDriven 的模型裏,是通過“裝配攔截器”裝配的。只要我們在裝配攔截器執行前,改變 ModelDriven 裏的模型對象就行了。這就需要我們自定義一個攔截器,struts2 提供了這個機制。在自定義的攔截器裏,我們根據用戶調用的 Action 方法,新建一個模型,並且將模型設置到 Action 中,這樣模型就可以是動態的了,記住,這個攔截器需要放在 defaultStack 的前面。

同樣,新的問題是如何根據 Action 方法動態的選擇業務模型呢?難道重複的寫 if 方法嗎?當然不能這樣,動態的模型,就應該來自於動態的方法。因此定義的 action 方法需要有規約,筆者在自己的程序中,是這樣定義規約的。Action 方法是這樣組成:_$_,使用美元符隔開(美元符是 Java 方法合法的標識符),前部分是操作名,可以任意取,後部分是業務模型的類名。同時,所有的業務模型,都放到指定的一個包裏,假設該包名爲 com.dw.business。那麼在自定義的攔截裏,我們獲得用戶調用的 Action 方法名,按美元符隔開獲得後半部分的類名,指定的包名(這裏是 com.dw.business)+ 類名就是業務模型類的全路徑,使用 Java 反射機制動態生成一個空的業務模型對象(所以,業務模型類必須有一個無參的構造函數),設置到 Action 裏,再交給裝配器的時候,裝配器會自動組裝這個模型。該攔截器的核心代碼如清單 4 所示。


清單 4. 攔截器清單
				 
 public String intercept(ActionInvocation ai) throws Exception {
		ai.addPreResultListener(this);
	Main action = (Main)ai.getAction();
	//業務方法名
	String name = ai.getInvocationContext().getName();
	int lastIndex = name.lastIndexOf("$");
	if (lastIndex != -1)
	{
	  try {
		String head = "com.dw.business.";
		String className = name.substring(lastIndex+1, name.length());
		//關鍵:動態設置業務模型
		action.setModel(Class.forName(head).newInstance());
	    } catch (Exception e) {}
	}
	return ai.invoke();
}


有了上面的配置,基本可以做到屏蔽大多數技術細節開發了,但是我們還有一個問題,當在業務方法中,意外拋出了異常,struts2 默認的是返回 INPUT,按照 清單 1 的配置,發生錯誤將跳轉到 noDir/error.jsp 頁面,顯示錯誤信息,這適合在非 Ajax 的處理。但是有時候我們希望他返回錯誤是 JSON 類型,因爲 Action 的一些方法是 Ajax 的調用方式,也就是 Action 方法的執行結果需要返回 errorJson。我的解決方法是利用 Java 註解技術,定義一個新的註解,名爲 IfErrorReturnToJson,它的代碼如清單 5 所示。


清單 5. IfErrorReturnToJson 的代碼
				 
 @Target(ElementType.METHOD) 
 @Retention(RetentionPolicy.RUNTIME) 
 public @interface IfErrorReturnToJson { 

 } 

該註解作用在方法上,也就是 Action 的業務方法,如果我們希望某業務方法在發送錯誤時,就返回 JSON 類型,那麼我們只需要在該方法上加上這個註解。現在,我們如何動態的修改返回值呢。清單 2 的第二行,爲 ActionInvocation 添加了 PreResultListener,這個監聽器是在返回結果前進行一些處理,正符合我們的需求。我們爲自定義攔截器類實現了 PreResultListener 接口,實現了接口方法,如清單 6 所示。


清單 6. PreResultListener 的代碼
				 
public void beforeResult(ActionInvocation ai, String result) {
		
	if (Main.INPUT.equals(result) || Main.ERROR.equals(result))
	{
		try {
			String methodName = ai.getInvocationContext().getName();
			Method method = Main.class.getMethod(methodName);
			if (method != null && method.getAnnotation(
			        IfErrorReturnToJson.class) != null)
			{
					ai.setResultCode(Main.ERROR_JSON);
			}
		} catch (Exception e) {
		}
	}
} 


可以看到,我們在返回結果前,如果當前的返回結果是 INPUT 或者 ERROR,我們會使用反射機制,檢查該方法是否加了 IfErrorReturnToJson 的註解,如果加了註解,則調用 ai.setResultCode(Main.ERROR_JSON); 方法,修改返回值,使得結果爲 JSON 數據類型。

事務隔離級別的規約

在 SSH 開發中,需要重點考慮的是 Spring 的事務配置。根據不同的業務,定義的事務隔離級別就不同,比如對隔離級別要求高的,就要用到 Spring 的序列化讀的隔離配置;有一些方法只是爲了權限控制,用於頁面跳轉,則不需要用到事務控制;一些操作目的是搜索,那麼該事務就是隻讀的。精細的事務配置,可以提高業務處理的代碼效率。這時候,業務方法的命名規約就可以起到隔離級別的控制作用,比如方法名是 *begin*( 方法名中包含 begin),則該方法沒有事務控制;方法名是 *search* 的,則該方法擁有隻讀的事務;方法名是 *seri*,則用序列化讀事務控制,提高併發安全級別。這個事務配置清單如所示。

權限規約

在一些應用中,涉及到簡單的權限管理。讀者可能會想到一些開源的中間件,如 ralasafe 這樣的開源權限組件,雖然很全面,但是學習起來需要一定的時間,熟練掌握,並配合 SSH 開發則更需要細緻的學習。當我們只是簡單的權限控制時,我們完全可以用到一些規約,來完成用戶的權限控制。在我設計的規約中,需要在數據庫中定義一張 Permission 表,代表權限 , 它與用戶表是多對一的關係,它至少有兩個字段,一個是 permissionName,一個是 permissionPrefix。permissionName 用於描述該權限,而 permissionPrefix 是我們所關心的。我們設計 Action 的方法,遵守 permissionPrefix_actionMethodName$BussinessBean 這樣的原則,permissionPrefix 前綴代表數據庫中權限表的 permissionPrefix 值 , 使用下劃線隔開。當用戶登錄後,會將該用戶所擁有的權限列表存放到 session 之中。當用戶試圖訪問一個 Action Method 時,會使用 ModelDriven 的規約 裏一樣的方法,截取方法名獲得該方法允許的訪問權限,如果用戶的權限列表中包含了該方法的權限,則允許調用,否則不允許調用。這樣設計,雖然無法做到數據的訪問權限,卻可以滿足一大部門的功能權限控制。這裏需要記住,權限描述可以修改,但是權限的 permissionPrefix 不能修改,如果方法名中沒有權限標識,則代表任何用戶都可以訪問。類似的,如果一個方法多個權限都可以訪問,則可以這樣設計方法 p1_p2_p3_methodName$BussinessBean,使用多個下劃線分割。

合理利用 Struts2 的傳參技術

基於 HTTP 協議的特殊性,上傳到 Web 服務器的數值都是字符串,而我們的業務模型都是對象類型,有一些還是複雜的業務對象。Struts2 提供了 Conveter 的機制,允許程序員將 String 轉換成複雜的業務對象。但是筆者剛用這個機制的時候,開始的確興奮,但是隨着業務的修改,業務模型的修改,這些 Conveter 的管理着實讓人頭疼。因此,本人認爲 Conveter 技術應該儘量少用。我提出了一個新的方法,假設一個業務模型中,裏面包含一個 List<People> pids 屬性對象,People 裏有一個 id 屬性,我們需要客戶端上傳 People 的 id 列表,放入 List<People> pids 之中。如果使用 Conveter,我們在客戶端需要定義規則,如客戶端上傳 pids=1,2,3,4,6,8。然後服務器端使用 Conveter 執行分割字符串、新建 People 對象、設置 ID、添加到列表等一系列操作。聽着都頭暈不是嗎 ? 而且這相對並不安全,對程序員解析字符串的功底要求很高。我們感謝 Struts2 的傳參機制,如果我們這樣傳參:<input name=”pids[0].id”/><input name=”pids[1].id”/>。這兩個 input 的上傳,Struts2 會根據上傳數據的 name,自動組裝成 List<People> 對象,並且賦值給 pids,pids[0] 代表列表的第 0 個元素,這用到了 OGNL 表達式的傳參規則,它會自動識別 pids 是數組還是列表,如果 pids 默認是空,它還會自動新建一個空的數組或列表。筆者可以查看 OGNL 的相關教程。合理利用該技術,可以大大縮減傳參的難度。

合理使用 Struts2 的標籤

Struts2 提供了衆多的標籤,這些標籤大致包括了 UI 標籤、數據標籤以及邏輯標籤。這裏,我們需要理解 UI 標籤與服務器端的數據交互格式,比如 checkboxlist 標籤,傳到服務器端就是一個數組,該數組存放的是選定的 checkbox 列表的數值 , 這比使用 iterator 標籤生成 checkbox 列表容易獲得數據。Doubleselect 標籤,用於生成 2 級的級聯 select,經常使用的是部門 - 用戶的級聯。Optiontransferselect 標籤,用於批量的遷移,比如用於批量的爲部門分配用戶。使用 struts2 的標籤可以大大減少用戶界面的開發,以及使得用戶界面到服務器的數據傳輸方式變得非常簡單。

Action 的模型驗證

Struts2 具有一套完善的驗證機制,在 ActionSupport 類裏,可以將模型驗證方法寫在 validate* 方法裏。如果重寫了 ActionSupport 的 validate 方法,這個 validate 會在執行所有 Action Method 前調用,執行驗證。而自定義的 validate*(* 是方法名),比如方法名是 add,則這個方法名就是 validateAdd),它只會在執行 add 方法前執行驗證。對於數據的驗證,筆者建議使用 validater 文件驗證,它更加容易配置和修改,利用現有的驗證器,可以減少硬編碼驗證的痛苦。在上面提到的框架中,由於使用的是動態方法機制,我們需要在 Action 類所在的包裏,新建 validator 的 XML 文件,名字是 { 類名 }-{ 方法名 }-validation.xml(如果不是動態的方法,則 { 類名 }-validation.xml 就足夠了)。最後的結果如圖 2 所示。


圖 2. Validator 文件配置結果
圖 2. Validator 文件配置結果 

具體的配置內容,讀者可以搜索 Struts2 的 validation 框架教程。

自定義業務模型驗證器

Struts2 提供的驗證器,包括 date、required、requiredstring 等等,這些可以歸於數據驗證。而對於特定的業務模型驗證,則比較複雜,因此,在該框架中,可以自定義一個業務驗證器,這是 struts2 支持的,它負責對業務模型進行驗證,比如可以驗證用戶上傳的用戶 ID 的用戶是否存在,這可以在很大程度上保證系統的安全性。驗證器的代碼清單 7 如下面所示。


清單 7. 業務驗證器代碼
				 
 public class BusinessValidator extends FieldValidatorSupport {

	private String property = null;
	
	public String getProperty() {
		return property;
	}
	
	public void setProperty(String property) {
		this.property = property;
	}
	
	@Override
	public void validate(Object exist) throws ValidationException {
		String fieldName = getFieldName();
		Object fieldValue = getFieldValue(fieldName, exist);
		if (fieldValue != null && fieldValue instanceof Integer && 
		((Integer)fieldValue) <= 0)
		{
			addFieldError("message", "上傳的ID,該數據是不存在!");
		} else if (fieldValue == null)
		{
			addFieldError("message", "上傳的ID,該數據是不存在!");
		} else {
			if (exist != null && exist instanceof Main && 
			((Main)exist).getModel() instanceof IChecker)
			{
				Main pa = (Main)exist;
				IChecker e = (IChecker)pa.getModel();
				boolean isRight = e.checkOk(property, pa);
				if (!isRight)
				{
					addFieldError("message", "上傳的ID,該數據是不存在!");
				} else {
					pa.getPubDao().getHibernateTemplate().clear();
				}
			}
		}
	}
} 


清單 7 中,用到一個 IChecker 接口,需要驗證的業務模型需要實現 IChecker 接口,在接口實現方法中實現業務驗證過程,錯誤的話返回 false。

在 src 目錄(或者 WEB-INF 下的 class 目錄),添加 validater.xml 文件,在其他自帶的驗證器後,添加業務驗證器配置,命名爲 check。


清單 8. 驗證器的配置
				 
 <validators> 
    ……
    <validator name="check" class="com.attendance.action.BusinessValidator"/> 
 </validators> 

接下來就可以使用這個驗證器,在圖 2 中的 validation 文件裏,添加如清單 9 的配置。


清單 9. 驗證器的使用
				 
 <field-validator type="check" short-circuit="true"> 
 <param name="property">department</param> 
 <message> 該部門不存在 !</message> 
 </field-validator> 

利用 JEE Eclipse 生成 Hibernate 的 JPA 模型

IBM 提供了 JEE 版的 Eclipse,專門用於開發 Java EE 的應用,它提供了從數據庫中生成符合 JPA 規範的數據模型的能力。Hibernate3 以後,支持了 JPA 規範(不是完全支持,但是已經較爲全面),利用 JEE Eclipse 的生成能力,就可以避免繁瑣的 hbm 文件的配置。同時,JPA 的規範裏,還可以提供 NamedQuery、OrderBy 等註解,對於複雜的數據庫,該規範可以減少開發時間。

UI 重用

Struts2 的 action 標籤,用於調用某個 action。這個標籤筆者認爲非常有用,尤其體現在 UI 重用中,比如用戶管理中,在很多個界面裏,都允許用戶信息修改和用戶刪除,那麼我們就可以將用戶信息修改和用戶刪除的頁面代碼以及 JavaScript 放在一個 JSP 裏,同時定義一個 Action 方法 ( 名爲 A),爲這個 JSP 執行初始化。接下來在允許進行用戶修改和刪除的界面,都用 s:action 標籤調用這個 A 方法,設置 executeResult 爲 true,將用戶修改和刪除的代碼包含在頁面裏。這樣就實現了 Action 的重用。如果重用的界面不需要服務器的初始化,這直接使用 jsp:include 或 s:include 引用重用的 JSP。

小結

本文通過一系列的講解,講述瞭如何搭建一個通用的 SSH 開發框架,陳述了其中的設計思想,由於篇幅限制,文章只介紹了框架中的重要部分和重要思想,希望讀者可以從中獲益。由於筆者水平有限,如有錯誤,請聯繫我批評指正。

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