基于注解的配置有越來越流行的趨勢,Spring 2.5 順應這種趨勢,為 Spring MVC 提供了完全基于注解的配置。本文将介紹 Spring 2.5 新增的 Sping MVC 注解功能,講述如何使用注解配置替換傳統的基于 XML 的 Spring MVC 配置。
<a>概述</a>
繼 Spring 2.0 對 Spring MVC 進行重大更新後,Spring 2.5 又為 Spring MVC 引入了注解驅動功能。現在你無須讓 Controller 繼承任何接口,無需在 XML 配置檔案中定義請求和 Controller 的映射關系,僅僅使用注解就可以讓一個 POJO 具有 Controller 的絕大部分功能 —— Spring MVC 架構的易用性得到了進一步的增強.在架構靈活性、易用性和擴充性上,Spring MVC 已經全面超越了其它的 MVC 架構,伴随着 Spring 一路高唱猛進,可以預見 Spring MVC 在 MVC 市場上的吸引力将越來越不可抗拒。
本文将介紹 Spring 2.5 新增的 Sping MVC 注解功能,講述如何使用注解配置替換傳統的基于 XML 的 Spring MVC 配置。

<a href="http://www.ibm.com/developerworks/cn/java/j-lo-spring25-mvc/index.html#main"><b>回頁首</b></a>
<a>一個簡單的基于注解的 Controller</a>
使用過低版本 Spring MVC 的讀者都知道:當建立一個 Controller 時,我們需要直接或間接地實作 org.springframework.web.servlet.mvc.Controller 接口。一般情況下,我們是通過繼承 SimpleFormController 或 MultiActionController 來定義自己的 Controller 的。在定義 Controller 後,一個重要的事件是在 Spring MVC 的配置檔案中通過 HandlerMapping 定義請求和控制器的映射關系,以便将兩者關聯起來。
來看一下基于注解的 Controller 是如何定義做到這一點的,下面是使用注解的 BbtForumController:
<a><b>清單 1. BbtForumController.java</b></a>
從上面代碼中,我們可以看出 BbtForumController 和一般的類并沒有差別,它沒有實作任何特殊的接口,因而是一個道地的 POJO。讓這個 POJO 與衆不同的魔棒就是 Spring MVC 的注解!
真正讓 BbtForumController 具備 Spring MVC Controller 功能的是 @RequestMapping 這個注解。@RequestMapping 可以标注在類定義處,将 Controller 和特定請求關聯起來;還可以标注在方法簽名處,以便進一步對請求進行分流。在 ① 處,我們讓 BbtForumController 關聯“/forum.do”的請求,而 ② 處,我們具體地指定 listAllBoard() 方法來處理請求。是以在類聲明處标注的 @RequestMapping 相當于讓 POJO 實作了 Controller 接口,而在方法定義處的 @RequestMapping 相當于讓 POJO 擴充 Spring 預定義的 Controller(如 SimpleFormController 等)。
為了讓基于注解的 Spring MVC 真正工作起來,需要在 Spring MVC 對應的 xxx-servlet.xml 配置檔案中做一些手腳。在此之前,還是先來看一下 web.xml 的配置吧:
<a><b>清單 2. web.xml:啟用 Spring 容器和 Spring MVC 架構</b></a>
web.xml 中定義了一個名為 annomvc 的 Spring MVC 子產品,按照 Spring MVC 的契約,需要在 WEB-INF/annomvc-servlet.xml 配置檔案中定義 Spring MVC 子產品的具體配置。annomvc-servlet.xml 的配置内容如下所示:
<a>清單 3. annomvc-servlet.xml</a>
因為 Spring 所有功能都在 Bean 的基礎上演化而來,是以必須事先将 Controller 變成 Bean,這是通過在類中标注 @Controller 并在 annomvc-servlet.xml 中啟用元件掃描機制來完成的,如 ① 所示。
在 ② 處,配置了一個 AnnotationMethodHandlerAdapter,它負責根據 Bean 中的 Spring MVC 注解對 Bean 進行加工處理,使這些 Bean 變成控制器并映射特定的 URL 請求。
而 ③ 處的工作是定義模型視圖名稱的解析規則,這裡我們使用了 Spring 2.5 的特殊命名空間,即 p 命名空間,它将原先需要通過 <property> 元素配置的内容轉化為 <bean> 屬性配置,在一定程度上簡化了 <bean> 的配置。
啟動 Tomcat,發送 [url]http://localhost/forum.do[/url] URL 請求,BbtForumController 的 listAllBoard() 方法将響應這個請求,并轉向 WEB-INF/jsp/listBoard.jsp 的視圖頁面。

<a>讓一個 Controller 處理多個 URL 請求</a>
在低版本的 Spring MVC 中,我們可以通過繼承 MultiActionController 讓一個 Controller 處理多個 URL 請求。使用 @RequestMapping 注解後,這個功能更加容易實作了。請看下面的代碼:
<a>清單 3. 每個請求處理參數對應一個 URL</a>
在這裡,我們分别在 ① 和 ② 處為 listAllBoard() 和 listBoardTopic() 方法标注了 @RequestMapping 注解,分别指定這兩個方法處理的 URL 請求,這相當于将 BbtForumController 改造為 MultiActionController。這樣 /listAllBoard.do 的 URL 請求将由 listAllBoard() 負責處理,而 /listBoardTopic.do?topicId=1 的 URL 請求則由 listBoardTopic() 方法處理。
對于處理多個 URL 請求的 Controller 來說,我們傾向于通過一個 URL 參數指定 Controller 處理方法的名稱(如 method=listAllBoard),而非直接通過不同的 URL 指定 Controller 的處理方法。使用 @RequestMapping 注解很容易實作這個常用的需求。來看下面的代碼:
<a><b>清單 4. 一個 Controller 對應一個 URL,由請求參數決定請求處理方法</b></a>
在類定義處标注的 @RequestMapping 讓 BbtForumController 處理所有包含 /bbtForum.do 的 URL 請求,而 BbtForumController 中的請求處理方法對 URL 請求的分流規則在 ② 和 ③ 處定義分流規則按照 URL 的 method 請求參數确定。是以分别在類定義處和方法定義處使用 @RequestMapping 注解,就可以很容易通過 URL 參數指定 Controller 的處理方法了。
@RequestMapping 注解中除了 params 屬性外,還有一個常用的屬性是 method,它可以讓 Controller 方法處理特定 HTTP 請求方式的請求,如讓一個方法處理 HTTP GET 請求,而另一個方法處理 HTTP POST 請求,如下所示:
<a><b>清單 4. 讓請求處理方法處理特定的 HTTP 請求方法</b></a>
這樣隻有當 /bbtForum.do?method=createTopic 請求以 HTTP POST 方式送出時,createTopic() 方法才會進行處理。

<a>處理方法入參如何綁定 URL 參數</a>
<a>按契約綁定</a>
Controller 的方法标注了 @RequestMapping 注解後,它就能處理特定的 URL 請求。我們不禁要問:請求處理方法入參是如何綁定 URL 參數的呢?在回答這個問題之前先來看下面的代碼:
<a>清單 5. 按參數名比對進行綁定</a>
當我們發送 [url]http://localhost//bbtForum.do?method=listBoardTopic&topicId=10[/url] 的 URL 請求時,Spring 不但讓 listBoardTopic() 方法處理這個請求,而且還将 topicId 請求參數在類型轉換後綁定到 listBoardTopic() 方法的 topicId 入參上。而 listBoardTopic() 方法的傳回類型是 String,它将被解析為邏輯視圖的名稱。也就是說 Spring 在如何給處理方法入參自動指派以及如何将處理方法傳回值轉化為 ModelAndView 中的過程中存在一套潛在的規則,不熟悉這個規則就不可能很好地開發基于注解的請求處理方法,是以了解這個潛在規則無疑成為了解 Spring MVC 架構基于注解功能的核心問題。
我們不妨從最常見的開始說起:請求處理方法入參的類型可以是 Java 基本資料類型或 String 類型,這時方法入參按參數名比對的原則綁定到 URL 請求參數,同時還自動完成 String 類型的 URL 請求參數到請求處理方法參數類型的轉換。下面給出幾個例子:
listBoardTopic(int topicId):和 topicId URL 請求參數綁定;
listBoardTopic(int topicId,String boardName):分别和 topicId、boardName URL 請求參數綁定;
特别的,如果入參是基本資料類型(如 int、long、float 等),URL 請求參數中一定要有對應的參數,否則将抛出 TypeMismatchException 異常,提示無法将 null 轉換為基本資料類型。
另外,請求處理方法的入參也可以一個 JavaBean,如下面的 User 對象就可以作為一個入參:
<a><b>清單 6. User.java:一個 JavaBean</b></a>
下面是将 User 作為 listBoardTopic() 請求處理方法的入參:
<a><b>清單 7. 使用 JavaBean 作為請求處理方法的入參</b></a>
這時,如果我們使用以下的 URL 請求:[url]http://localhost/bbtForum.do?method=listBoardTopic&topicId=1&userId=10&userName=tom[/url]
topicId URL 參數将綁定到 topicId 入參上,而 userId 和 userName URL 參數将綁定到 user 對象的 userId 和 userName 屬性中。和 URL 請求中不允許沒有 topicId 參數不同,雖然 User 的 userId 屬性的類型是基本資料類型,但如果 URL 中不存在 userId 參數,Spring 也不會報錯,此時 user.userId 值為 0。如果 User 對象擁有一個 dept.deptId 的級聯屬性,那麼它将和 dept.deptId URL 參數綁定。
<a>通過注解指定綁定的 URL 參數</a>
如果我們想改變這種預設的按名稱比對的政策,比如讓 listBoardTopic(int topicId,User user) 中的 topicId 綁定到 id 這個 URL 參數,那麼可以通過對入參使用 @RequestParam 注解來達到目的:
<a>清單 8. 通過 @RequestParam 注解指定</a>
這裡,對 listBoardTopic() 請求處理方法的 topicId 入參标注了 @RequestParam("id") 注解,是以它将和 id 的 URL 參數綁定。
<a>綁定模型對象中某個屬性</a>
Spring 2.0 定義了一個 org.springframework.ui.ModelMap 類,它作為通用的模型資料承載對象,傳遞資料供視圖所用。我們可以在請求處理方法中聲明一個 ModelMap 類型的入參,Spring 會将本次請求模型對象引用通過該入參傳遞進來,這樣就可以在請求處理方法内部通路模型對象了。來看下面的例子:
<a><b>清單 9. 使用 ModelMap 通路請示對應的隐含模型對象</b></a>
對于當次請求所對應的模型對象來說,其所有屬性都将存放到 request 的屬性清單中。象上面的例子,ModelMap 中的 currUser 屬性将放到 request 的屬性清單中,是以可以在 JSP 視圖頁面中通過 request.getAttribute(“currUser”) 或者通過 ${currUser} EL 表達式通路模型對象中的 user 對象。從這個角度上看, ModelMap 相當于是一個向 request 屬性清單中添加對象的一條管道,借由 ModelMap 對象的支援,我們可以在一個不依賴 Servlet API 的 Controller 中向 request 中添加屬性。
在預設情況下,ModelMap 中的屬性作用域是 request 級别是,也就是說,當本次請求結束後,ModelMap 中的屬性将銷毀。如果希望在多個請求中共享 ModelMap 中的屬性,必須将其屬性轉存到 session 中,這樣 ModelMap 的屬性才可以被跨請求通路。
Spring 允許我們有選擇地指定 ModelMap 中的哪些屬性需要轉存到 session 中,以便下一個請求屬對應的 ModelMap 的屬性清單中還能通路到這些屬性。這一功能是通過類定義處标注 @SessionAttributes 注解來實作的。請看下面的代碼:
<a><b>清單 10. 使模型對象的特定屬性具有 Session 範圍的作用域</b></a>
我們在 ② 處添加了一個 ModelMap 屬性,其屬性名為 currUser,而 ① 處通過 @SessionAttributes 注解将 ModelMap 中名為 currUser 的屬性放置到 Session 中,是以我們不但可以在 listBoardTopic() 請求所對應的 JSP 視圖頁面中通過 request.getAttribute(“currUser”) 和 session.getAttribute(“currUser”) 擷取 user 對象,還可以在下一個請求所對應的 JSP 視圖頁面中通過 session.getAttribute(“currUser”) 或 ModelMap#get(“currUser”) 通路到這個屬性。
這裡我們僅将一個 ModelMap 的屬性放入 Session 中,其實 @SessionAttributes 允許指定多個屬性。你可以通過字元串數組的方式指定多個屬性,如 @SessionAttributes({“attr1”,”attr2”})。此外,@SessionAttributes 還可以通過屬性類型指定要 session 化的 ModelMap 屬性,如 @SessionAttributes(types = User.class),當然也可以指定多個類,如 @SessionAttributes(types = {User.class,Dept.class}),還可以聯合使用屬性名和屬性類型指定:@SessionAttributes(types = {User.class,Dept.class},value={“attr1”,”attr2”})。
上面講述了如何往ModelMap中放置屬性以及如何使ModelMap中的屬性擁有Session域的作用範圍。除了在JSP視圖頁面中通過傳統的方法通路ModelMap中的屬性外,讀者朋友可能會問:是否可以将ModelMap中的屬性綁定到請求處理方法的入參中呢?答案是肯定的。Spring為此提供了一個@ModelAttribute的注解,下面是使用@ModelAttribute注解的例子:
<a>清單 11. 使模型對象的特定屬性具有 Session 範圍的作用域</a>
在 ② 處,我們向 ModelMap 中添加一個名為 currUser 的屬性,而 ① 外的注解使這個 currUser 屬性擁有了 session 級的作用域。是以,我們可以在 ③ 處通過 @ModelAttribute 注解将 ModelMap 中的 currUser 屬性綁定以請求處理方法的 user 入參中。
是以當我們先調用以下 URL 請求: [url]http://localhost/bbtForum.do?method=listBoardTopic&id=1&userName=tom&dept.deptId=12[/url]
以執行listBoardTopic()請求處理方法,然後再通路以下URL: [url]http://localhost/sample/bbtForum.do?method=listAllBoard[/url]
你将可以看到 listAllBoard() 的 user 入參已經成功綁定到 listBoardTopic() 中注冊的 session 級的 currUser 屬性上了。

<a>請求處理方法的簽名規約</a>
<a>方法入參</a>
我們知道标注了 @RequestMapping 注解的 Controller 方法就成為了請求處理方法,Spring MVC 允許極其靈活的請求處理方法簽名方式。對于方法入參來說,它允許多種類型的入參,通過下表進行說明:
請求處理方法入參的可選類型
說明
Java 基本資料類型和 String
預設情況下将按名稱比對的方式綁定到 URL 參數上,可以通過 @RequestParam 注解改變預設的綁定規則
request/response/session
既可以是 Servlet API 的也可以是 Portlet API 對應的對象,Spring 會将它們綁定到 Servlet 和 Portlet 容器的相應對象上
org.springframework.web.context.request.WebRequest
内部包含了 request 對象
java.util.Locale
綁定到 request 對應的 Locale 對象上
java.io.InputStream/java.io.Reader
可以借此通路 request 的内容
java.io.OutputStream / java.io.Writer
可以借此操作 response 的内容
任何标注了 @RequestParam 注解的入參
被标注 @RequestParam 注解的入參将綁定到特定的 request 參數上。
java.util.Map / org.springframework.ui.ModelMap
它綁定 Spring MVC 架構中每個請求所建立的潛在的模型對象,它們可以被 Web 視圖對象通路(如 JSP)
指令/表單對象(注:一般稱綁定使用 HTTP GET 發送的 URL 參數的對象為指令對象,而稱綁定使用 HTTP POST 發送的 URL 參數的對象為表單對象)
它們的屬性将以名稱比對的規則綁定到 URL 參數上,同時完成類型的轉換。而類型轉換的規則可以通過 @InitBinder 注解或通過 HandlerAdapter 的配置進行調整
org.springframework.validation.Errors / org.springframework.validation.BindingResult
為屬性清單中的指令/表單對象的校驗結果,注意檢驗結果參數必須緊跟在指令/表單對象的後面
rg.springframework.web.bind.support.SessionStatus
可以通過該類型 status 對象顯式結束表單的處理,這相當于觸發 session 清除其中的通過 @SessionAttributes 定義的屬性
Spring MVC 架構的易用之處在于,你可以按任意順序定義請求處理方法的入參(除了 Errors 和 BindingResult 必須緊跟在指令對象/表單參數後面以外),Spring MVC 會根據反射機制自動将對應的對象通過入參傳遞給請求處理方法。這種機制讓開發者完全可以不依賴 Servlet API 開發控制層的程式,當請求處理方法需要特定的對象時,僅僅需要在參數清單中聲明入參即可,不需要考慮如何擷取這些對象,Spring MVC 架構就象一個大管家一樣“不辭辛苦”地為我們準備好了所需的一切。下面示範一下使用 SessionStatus 的例子:
<a><b>清單 12. 使用 SessionStatus 控制 Session 級别的模型屬性</b></a>
processSubmit() 方法中的 owner 表單對象将綁定到 ModelMap 的“owner”屬性中,result 參數用于存放檢驗 owner 結果的對象,而 status 用于控制表單處理的狀态。在 ② 處,我們通過調用 status.setComplete() 方法,該 Controller 所有放在 session 級别的模型屬性資料将從 session 中清空。
<a>方法傳回參數</a>
在低版本的 Spring MVC 中,請求處理方法的傳回值類型都必須是 ModelAndView。而在 Spring 2.5 中,你擁有多種靈活的選擇。通過下表進行說明:
void
此時邏輯視圖名由請求處理方法對應的 URL 确定,如以下的方法:
對應的邏輯視圖名為“welcome”
String
此時邏輯視圖名為傳回的字元,如以下的方法:
對應的邏輯視圖名為“ownerForm”
org.springframework.ui.ModelMap
和傳回類型為 void 一樣,邏輯視圖名取決于對應請求的 URL,如下面的例子:
對應的邏輯視圖名為“vets”,傳回的 ModelMap 将被作為請求對應的模型對象,可以在 JSP 視圖頁面中通路到。
ModelAndView
當然還可以是傳統的 ModelAndView。
應該說使用 String 作為請求處理方法的傳回值類型是比較通用的方法,這樣傳回的邏輯視圖名不會和請求 URL 綁定,具有很大的靈活性,而模型資料又可以通過 ModelMap 控制。當然直接使用傳統的 ModelAndView 也不失為一個好的選擇。

<a>注冊自己的屬性編輯器</a>
Spring MVC 有一套常用的屬性編輯器,這包括基本資料類型及其包裹類的屬性編輯器、String 屬性編輯器、JavaBean 的屬性編輯器等。但有時我們還需要向 Spring MVC 架構注冊一些自定義的屬性編輯器,如特定時間格式的屬性編輯器就是其中一例。
Spring MVC 允許向整個 Spring 架構注冊屬性編輯器,它們對所有 Controller 都有影響。當然 Spring MVC 也允許僅向某個 Controller 注冊屬性編輯器,對其它的 Controller 沒有影響。前者可以通過 AnnotationMethodHandlerAdapter 的配置做到,而後者則可以通過 @InitBinder 注解實作。
下面先看向整個 Spring MVC 架構注冊的自定義編輯器:
<a><b>清單 13. 注冊架構級的自定義屬性編輯器</b></a>
MyBindingInitializer 實作了 WebBindingInitializer 接口,在接口方法中通過 binder 注冊多個自定義的屬性編輯器,其代碼如下所示:
<a><b>清單 14.自定義屬性編輯器</b></a>
如果希望某個屬性編輯器僅作用于特定的 Controller,可以在 Controller 中定義一個标注 @InitBinder 注解的方法,可以在該方法中向 Controller 了注冊若幹個屬性編輯器,來看下面的代碼:
<a><b>清單 15. 注冊 Controller 級的自定義屬性編輯器</b></a>
注意被标注 @InitBinder 注解的方法必須擁有一個 WebDataBinder 類型的入參,以便 Spring MVC 架構将注冊屬性編輯器的 WebDataBinder 對象傳遞進來。

<a>如何準備資料</a>
在編寫 Controller 時,常常需要在真正進入請求處理方法前準備一些資料,以便請求處理或視圖渲染時使用。在傳統的 SimpleFormController 裡,是通過複寫其 referenceData() 方法來準備引用資料的。在 Spring 2.5 時,可以将任何一個擁有傳回值的方法标注上 @ModelAttribute,使其傳回值将會進入到模型對象的屬性清單中。來看下面的例子:
<a><b>清單 16. 定義為處理請求準備資料的方法</b></a>
在 ① 處,通過使用 @ModelAttribute 注解,populateItem() 方法将在任何請求處理方法執行前調用,Spring MVC 會将該方法傳回值以“items”為名放入到隐含的模型對象屬性清單中。
是以在 ② 處,我們就可以通過 ModelMap 入參通路到 items 屬性,當執行 listAllBoard() 請求處理方法時,② 處将在控制台列印出“model.items:2”的資訊。當然我們也可以在請求的視圖中通路到模型對象中的 items 屬性。

<a>小結</a>
Spring 2.5 對 Spring MVC 進行了很大增強,現在我們幾乎完全可以使用基于注解的 Spring MVC 完全替換掉原來基于接口 Spring MVC 程式。基于注解的 Spring MVC 比之于基于接口的 Spring MVC 擁有以下幾點好處:
友善請求和控制器的映射;
友善請求處理方法入參綁定URL參數;
Controller 不必繼承任何接口,它僅是一個簡單的 POJO。
但是基于注解的 Spring MVC 并不完美,還存在優化的空間,因為在某些配置上它比基于 XML 的配置更繁瑣。比如對于處理多個請求的 Controller 來說,假設我們使用一個 URL 參數指定調用的處理方法(如 xxx.do?method=listBoardTopic),當使用注解時,每個請求處理方法都必須使用 @RequestMapping() 注解指定對應的 URL 參數(如 @RequestMapping(params = "method=listBoardTopic")),而在 XML 配置中我們僅需要配置一個 ParameterMethodNameResolver 就可以了。
<a>參考資料</a>
<b>學習</b>
<b>獲得産品和技術</b>
本文轉自 tony_action 51CTO部落格,原文連結:http://blog.51cto.com/tonyaction/69645,如需轉載請自行聯系原作者