繼 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 配置。
使用過低版本 Spring MVC 的讀者都知道:當創建一個 Controller 時,我們需要直接或間接地實現 org.springframework.web.servlet.mvc.Controller 接口。一般情況下,我們是通過繼承 SimpleFormController 或 MultiActionController 來定義自己的 Controller 的。在定義 Controller 后,一個重要的事件是在 Spring MVC 的配置文件中通過 HandlerMapping 定義請求和控制器的映射關系,以便將兩者關聯起來。
來看一下基於注解的 Controller 是如何定義做到這一點的,下面是使用注解的 BbtForumController:
package com.baobaotao.web; import com.baobaotao.service.BbtForumService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import java.util.Collection; @Controller //<——① @RequestMapping("/forum.do") public class BbtForumController { @Autowired private BbtForumService bbtForumService; @RequestMapping //<——② public String listAllBoard() { bbtForumService.getAllBoard(); System.out.println("call listAllBoard method."); return "listBoard"; } } |
從上面代碼中,我們可以看出 BbtForumController 和一般的類並沒有區別,它沒有實現任何特殊的接口,因而是一個地道的 POJO。讓這個 POJO 與眾不同的魔棒就是 Spring MVC 的注解!
在 ① 處使用了兩個注解,分別是 @Controller 和 @RequestMapping。在“使用 Spring 2.5 基於注解驅動的 IoC”這篇文章里,筆者曾經指出過 @Controller、@Service 以及 @Repository 和 @Component 注解的作用是等價的:將一個類成為 Spring 容器的 Bean。由於 Spring MVC 的 Controller 必須事先是一個 Bean,所以 @Controller 注解是不可缺少的。
真正讓 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 的配置吧:
清單 2. web.xml:啟用 Spring 容器和 Spring MVC 框架
web.xml 中定義了一個名為 annomvc 的 Spring MVC 模塊,按照 Spring MVC 的契約,需要在 WEB-INF/annomvc-servlet.xml 配置文件中定義 Spring MVC 模塊的具體配置。annomvc-servlet.xml 的配置內容如下所示:
因為 Spring 所有功能都在 Bean 的基礎上演化而來,所以必須事先將 Controller 變成 Bean,這是通過在類中標注 @Controller 並在 annomvc-servlet.xml 中啟用組件掃描機制來完成的,如 ① 所示。
在 ② 處,配置了一個 AnnotationMethodHandlerAdapter,它負責根據 Bean 中的 Spring MVC 注解對 Bean 進行加工處理,使這些 Bean 變成控制器並映射特定的 URL 請求。
而 ③ 處的工作是定義模型視圖名稱的解析規則,這里我們使用了 Spring 2.5 的特殊命名空間,即 p 命名空間,它將原先需要通過 <property> 元素配置的內容轉化為 <bean> 屬性配置,在一定程度上簡化了 <bean> 的配置。
啟動 Tomcat,發送 http://localhost/forum.do URL 請求,BbtForumController 的 listAllBoard() 方法將響應這個請求,並轉向 WEB-INF/jsp/listBoard.jsp 的視圖頁面。
在低版本的 Spring MVC 中,我們可以通過繼承 MultiActionController 讓一個 Controller 處理多個 URL 請求。使用 @RequestMapping 注解后,這個功能更加容易實現了。請看下面的代碼:
package com.baobaotao.web; import com.baobaotao.service.BbtForumService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @Controller public class BbtForumController { @Autowired private BbtForumService bbtForumService; @RequestMapping("/listAllBoard.do") // <—— ① public String listAllBoard() { bbtForumService.getAllBoard(); System.out.println("call listAllBoard method."); return "listBoard"; } @RequestMapping("/listBoardTopic.do") // <—— ② public String listBoardTopic(int topicId) { bbtForumService.getBoardTopics(topicId); System.out.println("call listBoardTopic method."); return "listTopic"; } } |
在這里,我們分別在 ① 和 ② 處為 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 注解很容易實現這個常用的需求。來看下面的代碼:
清單 4. 一個 Controller 對應一個 URL,由請求參數決定請求處理方法
package com.baobaotao.web; import com.baobaotao.service.BbtForumService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @Controller @RequestMapping("/bbtForum.do") // <—— ① 指定控制器對應URL請求 public class BbtForumController { @Autowired private BbtForumService bbtForumService; // <—— ② 如果URL請求中包括"method=listAllBoard"的參數,由本方法進行處理 @RequestMapping(params = "method=listAllBoard") public String listAllBoard() { bbtForumService.getAllBoard(); System.out.println("call listAllBoard method."); return "listBoard"; } // <—— ③ 如果URL請求中包括"method=listBoardTopic"的參數,由本方法進行處理 @RequestMapping(params = "method=listBoardTopic") public String listBoardTopic(int topicId) { bbtForumService.getBoardTopics(topicId); System.out.println("call listBoardTopic method."); return "listTopic"; } } |
在類定義處標注的 @RequestMapping 讓 BbtForumController 處理所有包含 /bbtForum.do 的 URL 請求,而 BbtForumController 中的請求處理方法對 URL 請求的分流規則在 ② 和 ③ 處定義分流規則按照 URL 的 method 請求參數確定。所以分別在類定義處和方法定義處使用 @RequestMapping 注解,就可以很容易通過 URL 參數指定 Controller 的處理方法了。
@RequestMapping 注解中除了 params 屬性外,還有一個常用的屬性是 method,它可以讓 Controller 方法處理特定 HTTP 請求方式的請求,如讓一個方法處理 HTTP GET 請求,而另一個方法處理 HTTP POST 請求,如下所示:
package com.baobaotao.web; import com.baobaotao.service.BbtForumService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @Controller @RequestMapping("/bbtForum.do") public class BbtForumController { @RequestMapping(params = "method=createTopic",method = RequestMethod.POST) public String createTopic(){ System.out.println("call createTopic method."); return "createTopic"; } } |
這樣只有當 /bbtForum.do?method=createTopic 請求以 HTTP POST 方式提交時,createTopic() 方法才會進行處理。
Controller 的方法標注了 @RequestMapping 注解后,它就能處理特定的 URL 請求。我們不禁要問:請求處理方法入參是如何綁定 URL 參數的呢?在回答這個問題之前先來看下面的代碼:
@RequestMapping(params = "method=listBoardTopic") //<—— ① topicId入參是如何綁定URL請求參數的? public String listBoardTopic(int topicId) { bbtForumService.getBoardTopics(topicId); System.out.println("call listBoardTopic method."); return "listTopic"; } |
當我們發送 http://localhost//bbtForum.do?method=listBoardTopic&topicId=10 的 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 對象就可以作為一個入參:
package com.baobaotao.web; public class User { private int userId; private String userName; //省略get/setter方法 public String toString(){ return this.userName +","+this.userId; } } |
下面是將 User 作為 listBoardTopic() 請求處理方法的入參:
@RequestMapping(params = "method=listBoardTopic") public String listBoardTopic(int topicId,User user) { bbtForumService.getBoardTopics(topicId); System.out.println("topicId:"+topicId); System.out.println("user:"+user); System.out.println("call listBoardTopic method."); return "listTopic"; } |
這時,如果我們使用以下的 URL 請求:http://localhost/bbtForum.do?method=listBoardTopic&topicId=1&userId=10&userName=tom
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 參數綁定。
如果我們想改變這種默認的按名稱匹配的策略,比如讓 listBoardTopic(int topicId,User user) 中的 topicId 綁定到 id 這個 URL 參數,那么可以通過對入參使用 @RequestParam 注解來達到目的:
package com.baobaotao.web; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; … @Controller @RequestMapping("/bbtForum.do") public class BbtForumController { @RequestMapping(params = "method=listBoardTopic") public String listBoardTopic(@RequestParam("id") int topicId,User user) { bbtForumService.getBoardTopics(topicId); System.out.println("topicId:"+topicId); System.out.println("user:"+user); System.out.println("call listBoardTopic method."); return "listTopic"; } … } |
這里,對 listBoardTopic() 請求處理方法的 topicId 入參標注了 @RequestParam("id") 注解,所以它將和 id 的 URL 參數綁定。
Spring 2.0 定義了一個 org.springframework.ui.ModelMap 類,它作為通用的模型數據承載對象,傳遞數據供視圖所用。我們可以在請求處理方法中聲明一個 ModelMap 類型的入參,Spring 會將本次請求模型對象引用通過該入參傳遞進來,這樣就可以在請求處理方法內部訪問模型對象了。來看下面的例子:
清單 9. 使用 ModelMap 訪問請示對應的隱含模型對象
@RequestMapping(params = "method=listBoardTopic") public String listBoardTopic(@RequestParam("id")int topicId, User user,ModelMap model) { bbtForumService.getBoardTopics(topicId); System.out.println("topicId:" + topicId); System.out.println("user:" + user); //① 將user對象以currUser為鍵放入到model中 model.addAttribute("currUser",user); return "listTopic"; } |
對於當次請求所對應的模型對象來說,其所有屬性都將存放到 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 注解來實現的。請看下面的代碼:
清單 10. 使模型對象的特定屬性具有 Session 范圍的作用域
package com.baobaotao.web; … import org.springframework.ui.ModelMap; import org.springframework.web.bind.annotation.SessionAttributes; @Controller @RequestMapping("/bbtForum.do") @SessionAttributes("currUser") //①將ModelMap中屬性名為currUser的屬性 //放到Session屬性列表中,以便這個屬性可以跨請求訪問 public class BbtForumController { … @RequestMapping(params = "method=listBoardTopic") public String listBoardTopic(@RequestParam("id")int topicId, User user, ModelMap model) { bbtForumService.getBoardTopics(topicId); System.out.println("topicId:" + topicId); System.out.println("user:" + user); model.addAttribute("currUser",user); //②向ModelMap中添加一個屬性 return "listTopic"; } } |
我們在 ② 處添加了一個 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注解的例子:
清單 11. 使模型對象的特定屬性具有 Session 范圍的作用域
package com.baobaotao.web; import com.baobaotao.service.BbtForumService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.ModelMap; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.SessionAttributes; import org.springframework.web.bind.annotation.ModelAttribute; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; @Controller @RequestMapping("/bbtForum.do") @SessionAttributes("currUser") //①讓ModelMap的currUser屬性擁有session級作用域 public class BbtForumController { @Autowired private BbtForumService bbtForumService; @RequestMapping(params = "method=listBoardTopic") public String listBoardTopic(@RequestParam("id")int topicId, User user, ModelMap model) { bbtForumService.getBoardTopics(topicId); System.out.println("topicId:" + topicId); System.out.println("user:" + user); model.addAttribute("currUser",user); //②向ModelMap中添加一個屬性 return "listTopic"; } @RequestMapping(params = "method=listAllBoard") //③將ModelMap中的 public String listAllBoard(@ModelAttribute("currUser") User user) { //currUser屬性綁定到user入參中。 bbtForumService.getAllBoard(); System.out.println("user:"+user); return "listBoard"; } } |
在 ② 處,我們向 ModelMap 中添加一個名為 currUser 的屬性,而 ① 外的注解使這個 currUser 屬性擁有了 session 級的作用域。所以,我們可以在 ③ 處通過 @ModelAttribute 注解將 ModelMap 中的 currUser 屬性綁定以請求處理方法的 user 入參中。
所以當我們先調用以下 URL 請求: http://localhost/bbtForum.do?method=listBoardTopic&id=1&userName=tom&dept.deptId=12
以執行listBoardTopic()請求處理方法,然后再訪問以下URL: http://localhost/sample/bbtForum.do?method=listAllBoard
你將可以看到 listAllBoard() 的 user 入參已經成功綁定到 listBoardTopic() 中注冊的 session 級的 currUser 屬性上了。
我們知道標注了 @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 的例子:
清單 12. 使用 SessionStatus 控制 Session 級別的模型屬性
@RequestMapping(method = RequestMethod.POST) public String processSubmit(@ModelAttribute Owner owner, BindingResult result, SessionStatus status) {//<——① new OwnerValidator().validate(owner, result); if (result.hasErrors()) { return "ownerForm"; } else { this.clinic.storeOwner(owner); status.setComplete();//<——② return "redirect:owner.do?ownerId=" + owner.getId(); } } |
processSubmit() 方法中的 owner 表單對象將綁定到 ModelMap 的“owner”屬性中,result 參數用於存放檢驗 owner 結果的對象,而 status 用於控制表單處理的狀態。在 ② 處,我們通過調用 status.setComplete() 方法,該 Controller 所有放在 session 級別的模型屬性數據將從 session 中清空。
在低版本的 Spring MVC 中,請求處理方法的返回值類型都必須是 ModelAndView。而在 Spring 2.5 中,你擁有多種靈活的選擇。通過下表進行說明: