SpringMVC 基於註解的Controller @RequestMapping @RequestParam

weixin_34162629發表於2017-06-06

概述

繼 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 配置。

一個簡單的基於註解的 Controller

使用過低版本號 Spring MVC 的讀者都知道:當建立一個 Controller 時,我們須要直接或間接地實現 org.springframework.web.servlet.mvc.Controller 介面。普通情況下,我們是通過繼承 SimpleFormController 或 MultiActionController 來定義自己的 Controller 的。

在定義 Controller 後。一個重要的事件是在 Spring MVC 的配置檔案裡通過 HandlerMapping 定義請求和控制器的對映關係,以便將兩者關聯起來。

來看一下基於註解的 Controller 是怎樣定義做到這一點的。以下是使用註解的 BbtForumController:


清單 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 的註解!

在 ① 處使用了兩個註解,各自是 @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 框架
                
<?

xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5"> <display-name>Spring Annotation MVC Sample</display-name> <!-- Spring 服務層的配置檔案 --> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:applicationContext.xml</param-value> </context-param> <!-- Spring 容器啟動監聽器 --> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener </listener-class> </listener> <!-- Spring MVC 的Servlet,它將載入WEB-INF/annomvc-servlet.xml 的 配置檔案,以啟動Spring MVC模組--> <servlet> <servlet-name>annomvc</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet </servlet-class> <load-on-startup>2</load-on-startup> </servlet> <servlet-mapping> <servlet-name>annomvc</servlet-name> <url-pattern>*.do</url-pattern> </servlet-mapping> </web-app>


web.xml 中定義了一個名為 annomvc 的 Spring MVC 模組,依照 Spring MVC 的契約,須要在 WEB-INF/annomvc-servlet.xml 配置檔案裡定義 Spring MVC 模組的詳細配置。annomvc-servlet.xml 的配置內容例如以下所看到的:

清單 3. annomvc-servlet.xml

                
<?xml version="1.0" encoding="UTF-8"?>
<beans 
    xmlns="http://www.springframework.org/schema/beans" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:p="http://www.springframework.org/schema/p" 
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans 
    http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
    http://www.springframework.org/schema/context 
    http://www.springframework.org/schema/context/spring-context-2.5.xsd">
     
    <!-- ①:對web包中的全部類進行掃描。以完畢Bean建立和自己主動依賴注入的功能 -->
    <context:component-scan base-package="com.baobaotao.web"/>

    <!-- ②:啟動Spring MVC的註解功能,完畢請求和註解POJO的對映 -->
    <bean class="org.springframework.web.servlet.mvc.annotation.
        AnnotationMethodHandlerAdapter"/>

    <!--  ③:對模型檢視名稱的解析,即在模型檢視名稱加入前字尾 -->
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" 
        p:prefix="/WEB-INF/jsp/" p:suffix=".jsp"/>
</beans>

由於 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 的檢視頁面。

讓一個 Controller 處理多個 URL 請求

在低版本號的 Spring MVC 中,我們能夠通過繼承 MultiActionController 讓一個 Controller 處理多個 URL 請求。

使用 @RequestMapping 註解後,這個功能更加easy實現了。請看以下的程式碼:

清單 3. 每一個請求處理引數相應一個 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
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 註解非常easy實現這個經常使用的需求。來看以下的程式碼:


清單 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 註解。就能夠非常easy通過 URL 引數指定 Controller 的處理方法了。

@RequestMapping 註解中除了 params 屬性外。另一個經常使用的屬性是 method,它能夠讓 Controller 方法處理特定 HTTP 請求方式的請求,如讓一個方法處理 HTTP GET 請求,而另一個方法處理 HTTP POST 請求。例如以下所看到的:


清單 4. 讓請求處理方法處理特定的 HTTP 請求方法
                
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() 方法才會進行處理。

處理方法入參怎樣繫結 URL 引數

按契約繫結

Controller 的方法標註了 @RequestMapping 註解後,它就能處理特定的 URL 請求。我們不禁要問:請求處理方法入參是怎樣繫結 URL 引數的呢?在回答這個問題之前先來看以下的程式碼:

清單 5. 按引數名匹配進行繫結

                
    @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 物件就能夠作為一個入參:


清單 6. User.java:一個 JavaBean
                
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() 請求處理方法的入參:


清單 7. 使用 JavaBean 作為請求處理方法的入參
                
    @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 引數繫結。

通過註解指定繫結的 URL 引數

假設我們想改變這樣的預設的按名稱匹配的策略,比方讓 listBoardTopic(int topicId,User user) 中的 topicId 繫結到 id 這個 URL 引數。那麼能夠通過對入參使用 @RequestParam 註解來達到目的:

清單 8. 通過 @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 中。你擁有多種靈活的選擇。

通過下表進行說明:

請求處理方法入參的可選型別 說明
void

此時邏輯檢視名由請求處理方法相應的 URL 確定。例如以下面的方法:

@RequestMapping("/welcome.do")
public void welcomeHandler() {
}

相應的邏輯檢視名為“welcome”

String

此時邏輯檢視名為返回的字元。例如以下面的方法:

@RequestMapping(method = RequestMethod.GET)
public String setupForm(@RequestParam("ownerId") int ownerId, ModelMap model) {
	Owner owner = this.clinic.loadOwner(ownerId);
	model.addAttribute(owner);
	return "ownerForm";
}

相應的邏輯檢視名為“ownerForm”

org.springframework.ui.ModelMap

和返回型別為 void 一樣,邏輯檢視名取決於相應請求的 URL。如以下的樣例:

@RequestMapping("/vets.do")
public ModelMap vetsHandler() {
	return new ModelMap(this.clinic.getVets());
}

相應的邏輯檢視名為“vets”,返回的 ModelMap 將被作為請求相應的模型物件,能夠在 JSP 檢視頁面中訪問到。

ModelAndView 當然還能夠是傳統的 ModelAndView。

應該說使用 String 作為請求處理方法的返回值型別是比較通用的方法,這樣返回的邏輯檢視名不會和請求 URL 繫結,具有非常大的靈活性。而模型資料又能夠通過 ModelMap 控制。當然直接使用傳統的 ModelAndView 也不失為一個好的選擇。

註冊自己的屬性編輯器

Spring MVC 有一套經常使用的屬性編輯器,這包含基本資料型別及其包裹類的屬性編輯器、String 屬性編輯器、JavaBean 的屬性編輯器等。

但有時我們還須要向 Spring MVC 框架註冊一些自己定義的屬性編輯器,如特定時間格式的屬性編輯器就是當中一例。

Spring MVC 同意向整個 Spring 框架註冊屬性編輯器,它們對全部 Controller 都有影響。

當然 Spring MVC 也同意僅向某個 Controller 註冊屬性編輯器。對其他的 Controller 沒有影響。前者能夠通過 AnnotationMethodHandlerAdapter 的配置做到,而後者則能夠通過 @InitBinder 註解實現。

以下先看向整個 Spring MVC 框架註冊的自己定義編輯器:


清單 13. 註冊框架級的自己定義屬性編輯器
                
>bean class="org.springframework.web.servlet.mvc.annotation.
AnnotationMethodHandlerAdapter"<
    >property name="webBindingInitializer"<
        >bean class="com.baobaotao.web.MyBindingInitializer"/<
    >/property<
>/bean<

MyBindingInitializer 實現了 WebBindingInitializer 介面,在介面方法中通過 binder 註冊多個自己定義的屬性編輯器,其程式碼例如以下所看到的:


清單 14.自己定義屬性編輯器
                
package org.springframework.samples.petclinic.web;

import java.text.SimpleDateFormat;
import java.util.Date;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.propertyeditors.CustomDateEditor;
import org.springframework.beans.propertyeditors.StringTrimmerEditor;
import org.springframework.samples.petclinic.Clinic;
import org.springframework.samples.petclinic.PetType;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.support.WebBindingInitializer;
import org.springframework.web.context.request.WebRequest;

public class MyBindingInitializer implements WebBindingInitializer {

    public void initBinder(WebDataBinder binder, WebRequest request) {
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        dateFormat.setLenient(false);
        binder.registerCustomEditor(Date.class, 
            new CustomDateEditor(dateFormat, false));
        binder.registerCustomEditor(String.class, new StringTrimmerEditor(false));
    }
}

假設希望某個屬性編輯器僅作用於特定的 Controller,能夠在 Controller 中定義一個標註 @InitBinder 註解的方法。能夠在該方法中向 Controller 了註冊若干個屬性編輯器,來看以下的程式碼:


清單 15. 註冊 Controller 級的自己定義屬性編輯器
                
@Controller
public class MyFormController {

    @InitBinder
    public void initBinder(WebDataBinder binder) {
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        dateFormat.setLenient(false);
        binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false));
    }
    …
}

注意被標註 @InitBinder 註解的方法必須擁有一個 WebDataBinder 型別的入參,以便 Spring MVC 框架將註冊屬性編輯器的 WebDataBinder 物件傳遞進來。

怎樣準備資料

在編寫 Controller 時,經常須要在真正進入請求處理方法前準備一些資料,以便請求處理或檢視渲染時使用。

在傳統的 SimpleFormController 裡,是通過複寫其 referenceData() 方法來準備引用資料的。在 Spring 2.5 時。能夠將不論什麼一個擁有返回值的方法標註上 @ModelAttribute,使其返回值將會進入到模型物件的屬性列表中。來看以下的樣例:


清單 16. 定義為處理請求準備資料的方法
                
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.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.SessionAttributes;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

@Controller
@RequestMapping("/bbtForum.do")
public class BbtForumController {

    @Autowired
    private BbtForumService bbtForumService;

    @ModelAttribute("items")//<——①向模型物件中加入一個名為items的屬性
    public List<String> populateItems() {
        List<String> lists = new ArrayList<String>();
        lists.add("item1");
        lists.add("item2");
        return lists;
    }

    @RequestMapping(params = "method=listAllBoard")
    public String listAllBoard(@ModelAttribute("currUser")User user, ModelMap model) {
        bbtForumService.getAllBoard();
        //<——②在此訪問模型中的items屬性
        System.out.println("model.items:" + ((List<String>)model.get("items")).size());
        return "listBoard";
    }
}

在 ① 處,通過使用 @ModelAttribute 註解,populateItem() 方法將在不論什麼請求處理方法執行前呼叫。Spring MVC 會將該方法返回值以“items”為名放入到隱含的模型物件屬性列表中。

所以在 ② 處,我們就能夠通過 ModelMap 入參訪問到 items 屬性。當執行 listAllBoard() 請求處理方法時,② 處將在控制檯列印出“model.items:2”的資訊。當然我們也能夠在請求的檢視中訪問到模型物件中的 items 屬性。

小結

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 就能夠了。


參考資料

學習

獲得產品和技術

相關文章