繼 Spring 2.0 對 Spring MVC 進行重大升級後,Spring 2.5 又爲 Spring MVC 引入了註解驅動功能。如今你無須讓 Controller 繼承任何接口,無需在 XML 配置文件中定義請求和 Controller 的映射關係,僅僅使用註解就可讓一個 POJO 具備 Controller 的絕大部分功能 —— Spring MVC 框架的易用性獲得了進一步的加強.在框架靈活性、易用性和擴展性上,Spring MVC 已經全面超越了其它的 MVC 框架,伴隨着 Spring 一路高唱猛進,能夠預見 Spring MVC 在 MVC 市場上的吸引力將愈來愈不可抗拒。web
本文將介紹 Spring 2.5 新增的 Sping MVC 註解功能,講述如何使用註解配置替換傳統的基於 XML 的 Spring MVC 配置。spring
回頁首數組
使用太低版本 Spring MVC 的讀者都知道:當建立一個 Controller 時,咱們須要直接或間接地實現 org.springframework.web.servlet.mvc.Controller 接口。通常狀況下,咱們是經過繼承 SimpleFormController 或 MultiActionController 來定義本身的 Controller 的。在定義 Controller 後,一個重要的事件是在 Spring MVC 的配置文件中經過 HandlerMapping 定義請求和控制器的映射關係,以便將二者關聯起來。mvc
來看一下基於註解的 Controller 是如何定義作到這一點的,下面是使用註解的 BbtForumController:app
清單 1. BbtForumController.java框架
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 的註解!jsp
在 ① 處使用了兩個註解,分別是 @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 請求參數到請求處理方法參數類型的轉換。下面給出幾個例子:
特別的,若是入參是基本數據類型(如 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 中,你擁有多種靈活的選擇。經過下表進行說明: