SpringMVC入門筆記

SpringMVC 細節方面的東西很多,所以在這里做一篇簡單的 SpringMVC 的筆記記錄,方便以后查看。

Spring MVC是當前最優秀的MVC框架,自從Spring 2.5版本發布后,由于支持注解配置,易用性有了大幅度的提高。Spring 3.0更加完善,實現了對老牌的MVC框架Struts 2的超越,現在版本已經到了Spring5.x了。

一、工程創建

1. 創建Maven的web工程,添加架包

Maven架包添加 spring-contextspring-webspring-webmvclog4j

2. 在web.xml中配置DispatcherServlet

<servlet>
  <servlet-name>dispatcherServlet</servlet-name>
  <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
  <init-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:spring-mvc.xml</param-value>
  </init-param>
  <load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
  <servlet-name>dispatcherServlet</servlet-name>
  <url-pattern>/</url-pattern>
</servlet-mapping>

注意:這里配置的 <url-pattern>/</url-pattern> 攔截資源配置的是 /,攔截所有除其他 servlet 之外的資源訪問,包括 jsp、靜態網頁、圖片等等。與 /* 不一樣,/* 一般配在攔截器里面,攔截所有資源訪問。

3. 創建SpringMVC的配置文件

上面配置 DispatcherServlet 里面用到了 contextConfigLocation 配置文件的地址,下面來創建配置文件。

<?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:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/mvc
       http://www.springframework.org/schema/mvc/spring-mvc.xsd">

    <!-- scan the package and the sub package -->
    <context:component-scan base-package="com.ogemray.springmvc"></context:component-scan>

    <!-- don't handle the static resource -->
    <mvc:default-servlet-handler />

    <!-- if you use annotation you must configure following setting -->
    <mvc:annotation-driven />

    <!-- configure the InternalResourceViewResolver -->
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/jsp/"></property>
        <property name="suffix" value=".jsp"></property>
    </bean>

    <!-- 配置首頁跳轉, 省略了在 Controller 里的創建訪問方法 -->
    <mvc:view-controller path="index" view-name="../index"></mvc:view-controller>

</beans>

二、@RequestMapping 注解

在對 SpringMVC 進行的配置的時候, 需要我們指定請求與處理方法之間的映射關系。 指定映射關系,就需要我們用上 @RequestMapping 注解。
@RequestMapping 是 Spring Web 應用程序中最常被用到的注解之一,這個注解會將 HTTP 請求映射到控制器(Controller類)的處理方法上。

1. value 和 method 屬性

簡單例子

@RequestMapping("rm")
@Controller
public class RequestMappingController {

    @RequestMapping(value = {"home", "/", ""}, method = RequestMethod.GET)
    public String goRMHome() {
        System.out.println("訪問了 Test RequestMapping 首頁");
        return "1-rm";
    }
}

最終訪問路徑是 .../rm/home,通過該方法返回視圖名字和SpringMVC視圖解析器加工,最終會轉發請求到 .../WEB-INF/jsp/1-rm.jsp 頁面。
如果沒有類名上面的 @RequestMapping("rm"),則訪問路徑為 .../home
method 指定方法請求類型,取值有 GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE。
value 為數組字符串,指定訪問路徑與方法對應,指定的地址可以是URI。URI值可以是中:普通的具體值、包含某變量、包含正則表達式。

下面以包含某一變量舉例:

@RequestMapping(value = "testPathVariable/{username}", method = RequestMethod.GET)
public String testPathVariable(@PathVariable(value = "username") String name) {
    //參數部分也可以直接寫成 @PathVariable String username, 省略value, 保證形參名與上面 {} 內的名字一致
    //不建議省略
    System.out.println("訪問了 Test PathVariable 方法 username: " + name);
    return "success";
}

2. consumes 屬性

指定處理請求的提交內容類型(Content-Type)

@RequestMapping(value = "testConsumes", method = RequestMethod.POST, consumes = "application/x-www-form-urlencoded")
public String testConsumes() {
    System.out.println("訪問了 Test Consumes 方法");
    return "success";
}

如果請求里面的 Content-Type 對不上會報錯

3. produces 屬性

指定返回的內容類型,僅當request請求頭中的(Accept)類型中包含該指定類型才返回

其中 */*;q=0.8 表明可以接收任何類型的,權重系數0.8表明如果前面幾種類型不能正常接收則使用該項進行自動分析。

@RequestMapping(value = "testProduces", method = RequestMethod.POST, produces = "text/html")
public String testProduces() {
    return "success";
}

4. params 屬性

指定request中必須包含某些參數值,才讓該方法處理

JSP頁面請求

<form action="${pageContext.request.contextPath}/rm/testParams" method="post">
    用戶名: <input type="text" name="username" value="Tom"><br>
    密  碼: <input type="text" name="password" value="123"><br>
    <input type="submit" value="測試 Test Params">
</form>

Controller 里面對應的請求方法

@RequestMapping(value = "testParams", method = RequestMethod.POST, params = {"username!=Tom", "password"})
public String testParams() {
    return "success";
}

params = {"username!=Tom", "password"} 表示請求參數里面 username !=Tom 且有包含 password,二者有一個不滿足則會報錯

5. headers 屬性

指定 request 中必須包含某些指定的 header 值,才能讓該方法處理請求

@RequestMapping(value = "testHeaders", method = RequestMethod.GET, headers = "Accept-Language=zh-CN,zh;q=0.9")
public String testHeaders() {
    return "success";
}

如果跟設定頭里面對不上會報404錯誤

三、@RequestParam注解

請求

<a href="${pageContext.request.contextPath}/rp/testGetOneParam?username=Tom">單參數 GET 請求方式</a>
1. 表單元素的name名字和控制器里的方法的形參名一致,此時可以省略 @RequestParam 注解
@RequestMapping(value = "testGetOneParam", method = RequestMethod.GET)
public String testGetOneParam(String username) {
    System.out.println("訪問了 單參數 Get 請求方法 username: " + username);
    return "success";
}

2. 不省略時的寫法

示例

@RequestMapping(value = "testPostOneParam", method = RequestMethod.POST)
public String testPostOneParam(@RequestParam String username) {
    System.out.println("username: " + name);
    return "success";
}

參數名字不一致時

@RequestMapping(value = "testPostOneParam", method = RequestMethod.POST)
public String testPostOneParam(@RequestParam(value = "username", required = false, defaultValue = "") String name) {
    System.out.println("username: " + name);
    return "success";
}

value 屬性指定傳過來的參數名,跟方法里的形參名字對應上
required 指定該參數是否是必須攜帶的
defaultValue 沒有或者為 null 時,指定默認值

注:省略和不省略 @RequestParam 注解,最終SpringMVC內部都是使用 RequestParamMethodArgumentResolver 參數解析器進行參數解析的。如果省略 @RequestParam 注解或省略 @RequestParam 注解的 value 屬性則最終則以形參的名字作為 keyHttpServletRequest 中取值。

四、@RequestHeader 和 @CookieValue 注解

@RequestHeader 注解:可以把 Request 請求 header 部分的值綁定到方法的參數上

@RequestMapping(value = "rh")
@Controller
public class RequestHeaderController {

    @RequestMapping(value = "testRHAccept", method = RequestMethod.GET)
    public String testRHAccept(@RequestHeader(value = "Accept") String accept) {
        System.out.println(accept);
        return "success";
    }

    @RequestMapping(value = "testRHAcceptEncoding", method = RequestMethod.GET)
    public String testRHAcceptEncoding(@RequestHeader(value = "Accept-Encoding") String acceptEncoding) {
        System.out.println(acceptEncoding);
        return "success";
    }
}

@CookieValue 注解:可以把Request header中關于cookie的值綁定到方法的參數上

@RequestMapping(value = "cv")
@Controller
public class CookieValueController {
    @RequestMapping(value = "testGetCookieValue", method = RequestMethod.GET)
    public String testGetCookieValue(@CookieValue(value = "JSESSIONID") String cookie) {
        System.out.println("獲取到Cookie里面 JSESSIONID 的值 " + cookie);
        return "success";
    }
}

五、數據結果封裝 ModelAndView & ModelMap & Map & Model

SpringMVC 為了方便數據封裝和處理,提供了以下幾種方案,最終會將封裝到模型里面的數據全都通過 request.setAttribute(name, value) 添加request請求域中。

1. ModelAndView

使用 ModelAndView 類用來存儲處理完后的結果數據,以及顯示該數據的視圖。從名字上看 ModelAndView 中的 Model 代表模型,View 代表視圖。modelModelMap 的類型,而 ModelMap 又是 LinkedHashMap 的子類,view 包含了一些視圖信息。

@RequestMapping(value = "testReturnModelAndView", method = RequestMethod.GET)
public ModelAndView testReturnModelAndView() {

    Student s1 = new Student(1, "Tom", 13, new Date());
    Student s2 = new Student(2, "Jerry", 14, new Date());

    List<Student> list = new ArrayList<>();
    list.add(s1); list.add(s2);

    HashMap<String, Student> map = new HashMap<>();
    map.put("s1", s1); map.put("s2", s2);

    ModelAndView mv = new ModelAndView();
    mv.addObject("s1", s1);
    mv.addObject("s2", s2);

    mv.addObject("list", list);
    mv.addObject("map", map);
    mv.setViewName("5-m&v-success");
    return mv;
}

2. ModelMap & Map & Model

最終也是將封裝的數據和返回視圖名字封裝成 ModelAndView對象

@RequestMapping(value = "testMapParam", method = RequestMethod.GET)
public String testMapParam(Map<String, Object> paramMap) {
    ...
    paramMap.put("s1", s1);
    paramMap.put("s2", s2);

    paramMap.put("list", list);
    paramMap.put("map", map);
    return "5-m&v-success";
}

@RequestMapping(value = "testModelParam", method = RequestMethod.GET)
public String testModelParam(Model model) {
    ...
    model.addAttribute("s1", s1);
    model.addAttribute("s2", s2);

    model.addAttribute("list", list);
    model.addAttribute("map", map);
    return "5-m&v-success";
}

@RequestMapping(value = "testModelMapParam", method = RequestMethod.GET)
public String testModelMapParam(ModelMap modelMap) {
    ...
    modelMap.addAttribute("s1", s1);
    modelMap.addAttribute("s2", s2);

    modelMap.addAttribute("list",list);
    modelMap.addAttribute("map", map);
    return "5-m&v-success";
}

3. JSP 頁面提取數據

<%@ page contentType="text/html;charset=UTF-8" language="java" pageEncoding="UTF-8" isELIgnored="false" %>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<body>

    <c:if test="${s1 != null && s2 != null}">
        <h3 align="center">單個數據封裝</h3>
        <table border="1px solid black" style="border-collapse: collapse" align="center">
            <tr><td colspan="2" align="center">s1</td></tr>
            <tr><td>姓名</td><td>${s1.name}</td></tr>
            <tr><td>年齡</td><td>${s1.age}</td></tr>
            <tr><td>生日</td><td>${s1.birthday.toString()}</td></tr>

            <tr><td colspan="2" align="center">s2</td></tr>
            <tr><td>姓名</td><td>${s2.name}</td></tr>
            <tr><td>年齡</td><td>${s2.age}</td></tr>
            <tr><td>生日</td><td>${s2.birthday.toString()}</td></tr>
        </table>
    </c:if>

    <c:if test="${list != null}">
        <h3 align="center">List數據封裝</h3>
        <table border="1px solid black" style="border-collapse: collapse" align="center">
            <c:forEach items="${list}" var="s" varStatus="status">
                <tr><td colspan="2" align="center">${status.count}</td></tr>
                <tr><td>姓名</td><td>${s.name}</td></tr>
                <tr><td>年齡</td><td>${s.age}</td></tr>
                <tr><td>生日</td><td>${s.birthday.toString()}</td></tr>
            </c:forEach>
        </table>
    </c:if>

    <c:if test="${map != null}">
        <h3 align="center">Map數據封裝</h3>
        <table border="1px solid black" style="border-collapse: collapse" align="center">
            <c:forEach items="${map}" var="node">
                <tr><td colspan="2" align="center">${node.key}</td></tr>
                <tr><td>姓名</td><td>${node.value.name}</td></tr>
                <tr><td>年齡</td><td>${node.value.age}</td></tr>
                <tr><td>生日</td><td>${node.value.birthday.toString()}</td></tr>
            </c:forEach>
        </table>
    </c:if>
</body>
</html>

六、@SessionAttributes

如果我們希望在多個請求之間共用某個模型屬性數據,則可以在控制器類上標注一個 @SessionAttributes,SpringMVC 將把模型中對應的屬性暫存到 HttpSession 的域中。

使用方法:
@SessionAttributes(value={"xxx"}, types={xxxx.class})
value:是通過鍵來指定放入HttpSession 的域中的值;
types:是通過類型指定放入HttpSession 的域中的值;

@SessionAttributes(types=Student.class)
這個注解會將類中所有放入 Request 域中的 Student 對象同時放進 HttpSession 的域空間中。

可以添加多個屬性
@SessionAttributes(value={“s1”, “s2”})
@SessionAttributes(types={User.class, Grade.class})

可以混合使用
@SessionAttributes(value={“s1”, “s2”},types={Grade.class})

示例

//@SessionAttributes(value = {"s1", "s2"})
@SessionAttributes(types = Student.class)
@RequestMapping(value = "sa")
@Controller
public class SessionAttributesController {

    @RequestMapping(value = "testSA", method = RequestMethod.GET)
    public String testSessionAttributes(Model model) {
        Student s1 = new Student(1, "Tom", 13, new Date());
        Student s2 = new Student(2, "Jerry", 13, new Date());

        model.addAttribute("s1", s1);
        model.addAttribute("s2", s2);
        return "6-sa-success";
    }
}

JSP 頁面提取數據

<%@ page contentType="text/html;charset=UTF-8" language="java" pageEncoding="UTF-8" isELIgnored="false" %>
<html>
<body>
    request s1 : ${requestScope.get("s1")}<br><br>
    request s2 : ${requestScope.get("s2")}<br><br>

    session s1 : ${sessionScope.get("s1")}<br><br>
    session s2 : ${sessionScope.get("s2")}<br><br>
</body>
</html>

七、@ModelAttribute

該注解平時使用的比較多,不僅可以寫在方法上面也可以寫在參數前面。

1. @ModelAttribute 寫在方法上面

  • 在同一個控制器中,標注了@ModelAttribute 的方法實際上會在 @RequestMapping 注解方法之前被調用。
  • 標注了@ModelAttribute 的方法能接受與@RequestMapping 標注相同的參數類型,只不過不能直接被映射到具體的請求上。
  • 標注在方法上的 @ModelAttribute 說明方法一般是用于添加一個或多個屬性到 model 上。

模擬請求

<a href="${pageContext.request.contextPath}/testModelAttribute">模擬請求</a>
① 省略 value 屬性值手動加入屬性
@ModelAttribute
public void modelAttributeMethod1(ModelMap modelMap) {
    Person person = new Person("超哥哥 1 號", 12);
    modelMap.addAttribute("person1", person);
}

@RequestMapping(value = "testModelAttribute", method = RequestMethod.GET)
public String testModelAttribute(ModelMap modelMap) {
    modelMap.forEach((key, value) -> {
        System.out.println(key + " = " + value);
        //person1 = Person{name='超哥哥 1 號', age=12}
    });
    return "success";
}

可以看出手動加入 model 里面屬性成功,key 為自定義的字符串。

② 省略 value 屬性值自動加入屬性
@ModelAttribute
public Person modelAttributeMethod2() {
    return new Person("超哥哥 2 號", 12);
}

@RequestMapping(value = "testModelAttribute", method = RequestMethod.GET)
public String testModelAttribute(ModelMap modelMap) {
    modelMap.forEach((key, value) -> {
        System.out.println(key + " = " + value);
        //person = Person{name='超哥哥 2 號', age=12}
    });
    return "success";
}

可以看出 @ModelAttribute 修飾的方法沒有指定 value 屬性時,讓其自動加入的 key 是以添加類的類名首字母小寫。

③ 指明 value 屬性值自動加入屬性
@ModelAttribute(value = "person3")
public Person modelAttributeMethod3() {
    return new Person("超哥哥 3 號", 13);
}

@RequestMapping(value = "testModelAttribute", method = RequestMethod.GET)
public String testModelAttribute(ModelMap modelMap) {
    modelMap.forEach((key, value) -> {
        System.out.println(key + " = " + value);
        //person3 = Person{name='超哥哥 3 號', age=13}
    });
    return "success";
}

從上面可以看出 @ModelAttribute 修飾的方法有指定 value 屬性時,讓其自動加入的 key 就是自定的 value 屬性的值。

2. @ModelAttribute 寫在參數前面

標注在方法參數前的 @ModelAttribute 說明了該方法參數的值將由 model 中取得,如果 model 中找不到,那么該參數會先被實例化,然后被添加到 model 中。在 model 中存在以后,將請求中所有名稱匹配的參數都填充到該參數對象上。

模擬請求

<a href="${pageContext.request.contextPath}/testModelAttribute?age=13">模擬請求</a>
① 省略 value 屬性值自動匹配或創建
@RequestMapping(value = "testModelAttribute", method = RequestMethod.GET)
public String testModelAttribute(@ModelAttribute Person person) {
    System.out.println(person);
    //Person{name='null', age=13}
    return "success";
}

注:在執行 testModelAttribute(..) 方法時,因為參數屬性是一個 Person 類對象,那么他先從 model 里面找(沒有指明 value 屬性值,則以該類名首字母小寫為 key),發現找不到便創建一個,把請求里面的參數賦值到該創建對象上,找到了則用請求里面的參數更新該對象。

② 指定 value 屬性值匹配或創建
@ModelAttribute(value = "p")
public Person modelAttributeMethod3(@RequestParam Integer age) {
    return new Person("超哥哥 3 號", age);
}

@RequestMapping(value = "testModelAttribute", method = RequestMethod.GET)
public String testModelAttribute(@ModelAttribute(value = "p") Person person) {
    System.out.println(person);
    //Person{name='超哥哥 3 號', age=13}
    return "success";
}

注:在執行 testModelAttribute(..) 方法時,因為參數屬性是一個 Person 類對象,那么他先從 model 里面找(有指明 value 屬性值,則以 value 屬性值為 key),發現找不到便創建一個,把請求里面的參數賦值到該創建對象上,找到了則用請求里面的參數更新該對象。

③ 省略 @ModelAttribute 注解的 POJO 參數
@ModelAttribute
public Person modelAttributeMethod3(@RequestParam Integer age) {
    return new Person("超哥哥 4 號", age);
}

@RequestMapping(value = "testModelAttribute", method = RequestMethod.GET)
public String testModelAttribute(Person person) {
    System.out.println(person);
    //Person{name='超哥哥 4 號', age=13}
    return "success";
}

注:@ModelAttribute 注解修飾的方法,沒有指定 value 屬性,則自動注入到 model 里面的 value 以該對象類名首字母小寫為 key。在下面 @RequestMapping 修飾的方法 testModelAttribute(..) 參數時一個 POJO 對象,雖前面沒有注解修飾,但默認也會去匹配 ModelAttributeMethodProcessor 參數解析器去解析該參數,說白了與上面的第一種情況 @ModelAttribute 注解修飾沒有設置 value 屬性值是一樣的。

八、在Controller中使用redirect方式處理請求

forword:表示轉發!
redirect:表示重定向!

@RequestMapping(value = "index")
public String index() {
    return "success";
}
@RequestMapping(value = "index")
public String index() {
    return "redirect:success";
}

九、RESTFul 風格的 SpringMVC

1. RESTFulController

@RequestMapping(value = "rest")
@Controller
public class RESTFulController {

    @RequestMapping(value = {"home", "/", ""}, method = RequestMethod.GET)
    public String goResetHome() {
        System.out.println("訪問了 Rest 風格測試首頁");
        return "8-rest";
    }

    @RequestMapping(value = "student/{id}", method = RequestMethod.GET)
    public String get(@PathVariable(value = "id") Integer id) {
        System.out.println("get " + id);
        return "success";
    }

    @RequestMapping(value = "student/{id}", method = RequestMethod.POST)
    public String post(@PathVariable(value = "id") Integer id) {
        System.out.println("post " + id);
        return "success";
    }

    @RequestMapping(value = "student/{id}", method = RequestMethod.PUT)
    public String put(@PathVariable(value = "id") Integer id) {
        System.out.println("put " + id);
        return "success";
    }

    @RequestMapping(value = "student/{id}", method = RequestMethod.DELETE)
    public String delete(@PathVariable(value = "id") Integer id) {
        System.out.println("delete " + id);
        return "success";
    }
}

2. form表單發送put和delete請求,需要在web.xml中進行如下配置

<!-- configure the HiddenHttpMethodFilter,convert the post method to put or delete -->
<filter>
  <filter-name>hiddenHttpMethodFilter</filter-name>
  <filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
</filter>
<filter-mapping>
  <filter-name>hiddenHttpMethodFilter</filter-name>
  <url-pattern>/*</url-pattern>
</filter-mapping>

3. 在前臺可以用以下代碼產生請求

<form action="${pageContext.request.contextPath}/rest/student/1" method="get">
    <input type="submit" value="GET">
</form>

<form action="${pageContext.request.contextPath}/rest/student/1" method="post">
    <input type="submit" value="POST">
</form>

<form action="${pageContext.request.contextPath}/rest/student/1" method="post">
    <input type="hidden" name="_method" value="PUT">
    <input type="submit" value="PUT">
</form>

<form action="${pageContext.request.contextPath}/rest/student/1" method="post">
    <input type="hidden" name="_method" value="DELETE">
    <input type="submit" value="DELETE">
</form>

十、@RequestBody 和 @ResponseBody

在SpringMVC的 Controller 中經常會用到 @RequestBody@ResponseBody 這兩個注解,若想使用這兩個注解,前提要寫好 <mvc:annotation-driven /> 標簽,他會幫我們注入接下里解析需要的轉換器。

1. @RequestBody

簡介:
@RequestBody 注解用于修飾 Controller 的方法參數,根據 HTTP Request Header 的 content-Type 的內容,通過適當的 HttpMessageConverter 轉換為 Java 類。

使用時機:
當提交的數據不是普通表單的形式(application/x-www-form-urlcodedmultipart/form-data),而是 JSON 格式(application/json) 或 XML 格式(application/xml)。

使用示例:XML格式數據提交

POJO 模型類

@XmlRootElement(name = "person")
public class Person {
    private String name;
    private Integer age;

    public String getName() { return name; }
    @XmlElement
    public void setName(String name) { this.name = name; }
    public Integer getAge() { return age; }
    @XmlElement
    public void setAge(Integer age) { this.age = age; }
}

AJAX 請求

<a id="tag" href="${pageContext.request.contextPath}/testRequestBody">點擊事件</a>

<script type="text/javascript">
    $("#tag").click(function () {
        var arg =
            "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>" +
                "<person>" +
                    "<name>Tom</name>" +
                    "<age>13</age>" +
                "</person>";
        $.ajax({
            url: this.href,
            type: "POST",
            data: arg,
            contentType: "application/xml;charset=utf-8",
            success: function (data, textStatus) {  },
            error: function (data, textStatus, errorThrown) {  }
        });
        return false;
    });
</script>

Controller 里對應的方法

@RequestMapping(value = "testRequestBody", method = RequestMethod.POST)
public String testRequestBody(@RequestBody Person person) {
    System.out.println(person);
    //Person{name='Tom', age=13}
    return "success";
}

注:@RequestBody 注解對于XML請求數據的解析,請求方要指定 Content-Type = application/xml;charset=utf-8,服務器如果要將接收數據封裝成 POJO 類,需要在該 POJO 類里面用 @XmlRootElement@XmlElement 注解指明跟標簽和子標簽,SpringMVC 內部最終用到的是自帶的 Jaxb2RootElementHttpMessageConverter 轉換器(其實現了 HttpMessageConverter 接口)。

2. @ResponseBody

簡介:
@ResponseBody 注解用于修飾 Controller 的方法,根據 HTTP Request Header 的 Accept 的內容,通過適當的 HttpMessageConverter 轉換為客戶端需要格式的數據并且寫入到 Responsebody 數據區,從而不通過視圖解析器直接將數據響應給客戶端。

使用時機:
返回的數據不是html標簽的頁面,而是其他某種格式的數據時(如json、xml等)使用。

使用示例:XML格式數據響應

POJO 模型類

@XmlRootElement(name = "person")
public class Person {
    private String name;
    private Integer age;

    public String getName() { return name; }
    @XmlElement
    public void setName(String name) { this.name = name; }
    public Integer getAge() { return age; }
    @XmlElement
    public void setAge(Integer age) { this.age = age; }
}

Controller 里對應的方法

@ResponseBody
@RequestMapping(value = "testRequestBody", method = RequestMethod.POST)
public Person testRequestBody() {
    Person person = new Person("Tom",13);
    return person;
}

AJAX 請求

<a id="tag" href="${pageContext.request.contextPath}/testRequestBody">點擊事件</a>

<script type="text/javascript">
    $("#tag").click(function () {
        $.ajax({
            url: this.href,
            type: "POST",
            data: null,
            headers: { Accept: "application/xml;charset=utf-8" },
            success: function (data, textStatus) {
                console.log(textStatus);
                console.log(data);
            },
            error: function (data, textStatus, errorThrown) {
                console.log(textStatus + "   " + data + "  " + errorThrown);
            }
        });
        return false;
    });
</script>

最終瀏覽器控制臺輸出

注:@ResponseBody 注解對于響應XML格式數據的解析,請求方要指定 Accept = application/xml;charset=utf-8,服務器如果想將 POJO 類轉換成XML格式數據,需要在該 POJO 類里面用 @XmlRootElement@XmlElement 注解指明跟標簽和子標簽,SpringMVC 內部最終用到的是自帶的 Jaxb2RootElementHttpMessageConverter 轉換器(其實現了 HttpMessageConverter 接口)。

3. 原理簡介

@RequestBody@ResponseBody 注解最終匹配到的參數解析器和返回值解析器都是 RequestResponseBodyMethodProcessor 對象,所以該對象分別實現了 HandlerMethodArgumentResolverHandlerMethodReturnValueHandler 接口。
在該解析器中有一個 messageConverters 屬性,該屬性是用來記錄轉換器的 List,這些轉換器都是在該解析器初始化的時候 <mvc:annotation-driven /> 標簽幫我們注入的。并且這些解析器都實現了 HttpMessageConverter 接口,在 HttpMessageConverter 接口中有四個最為主要的接口方法。

public interface HttpMessageConverter<T> {
    boolean canRead(Class<?> clazz, @Nullable MediaType mediaType);
    T read(Class<? extends T> clazz, HttpInputMessage inputMessage);

    boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType);
    void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage);
}

read 對應請求輸入的轉換解析,write 對應響應輸出的轉換解析。
canRead 根據 Request Header 的 content-Type 的內容查看該 HttpMessageConverter 換器是否支持轉換,支持則轉換為對應的 Java 類綁定到修飾的方法入參上。
canWrite 根據 Request Headers 里面的 Accept 的內容查看該 HttpMessageConverter 換器是否支持轉換,支持則轉換成指定格式后,寫入到 Response 對象的 body 數據區。

對應流程圖如下

十一、解析和返回 Json 數據

1. 首先需要導入 JSON 支持架包并且注入轉換器

<dependency>
  <groupId>com.fasterxml.jackson.core</groupId>
  <artifactId>jackson-databind</artifactId>
  <version>2.9.6</version>
</dependency>

jackson-databind-2.9.6.jar 架包依賴于 jackson-annotations-2.9.0.jarjackson-core-2.9.6.jar,所以省略了依賴架包的手動導入。

同時要寫好 <mvc:annotation-driven /> 標簽,其會幫我們注入對應的JSON數據轉換器。

2. 代碼示例

需要封裝的 POJO

public class Person {
    private String name;
    private Integer age;
}

Controller中對應的請求方法

@ResponseBody
@RequestMapping(value = "testRequestBody", method = RequestMethod.POST)
public Person testRequestBody(@RequestBody Person person) {
    System.out.println(person);
    return person;
}

注:參數用 @RequestBody 修飾意思是將請求的JSON數據用合適的轉換器,轉換成 Java 類。@ResponseBody 注解是將返回的數據通過合適的轉換器轉換成客戶端想要的樣子并返回,在這里是將請求解析的 Person 對象轉換成JOSN格式數據并返回。

AJAX 請求

<a id="tag" href="${pageContext.request.contextPath}/testRequestBody">點擊事件</a>

<script type="text/javascript">
    $("#tag").click(function () {
        var arg = {name : "Tom", age : "10"};
        $.ajax({
            url: this.href,
            type: "POST",
            data: JSON.stringify(arg),
            contentType: "application/json;charset=utf-8",
            headers: { Accept: "application/json;charset=utf-8" },
            success: function (data, textStatus) {
                console.log(textStatus);
                console.log(data);
            },
            error: function (data, textStatus, errorThrown) {
                console.log(textStatus + "   " + data + "  " + errorThrown);
            },
        });
        return false;
    });
</script>

注:① 發送的數據要是JSON格式(也就是 data 屬性的數據是JSON格式);② 指明請求數據為JSON格式(contentType: "application/json;charset=utf-8");③ 指明接收數據為JSON格式(headers: { Accept: "application/json;charset=utf-8" })。

3. 原理簡介

最終使用到的轉換器是 jackson 提供的 MappingJackson2HttpMessageConverter,也是在解析器初始化的時候 <mvc:annotation-driven /> 標簽幫我們注入的。

十二、文件上傳

1. 導入文件上傳支持架包

為了實現文件上傳,需要導入 commons-fileupload 架包,導入如下

<!-- https://mvnrepository.com/artifact/commons-fileupload/commons-fileupload -->
<dependency>
    <groupId>commons-fileupload</groupId>
    <artifactId>commons-fileupload</artifactId>
    <version>1.3.3</version>
</dependency>

2. 配置 MultipartResolver

SpringMVC 上下文中默認沒有裝配 MultipartResolver,因此默認情況下其不能處理文件上傳工作。如果想使用SpringMVC的文件上傳功能,則需要在上下文中配置 MultipartResolver。在SpringMVC配置文件中進行如下配置

<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
    <!-- 請求的編碼格式,必須和 jsp 的 pageEncoding 屬性一致,默認為ISO-8859-1 -->
    <property name="defaultEncoding" value="utf-8"></property>
    <!-- 上傳最大限制 1M = 1M * 1024 * 1024 = 1048576 byte-->
    <property name="maxUploadSize" value="1048576"></property>
</bean>

注:這里一定要設置 id,并且值必須是 multipartResolver,下面的簡單原理會解釋。

3. 代碼示例

Controller 中對應的方法

@RequestMapping(value = "upload", method = RequestMethod.POST)
public String testUpload(@RequestParam(value = "file") MultipartFile multipartFile, HttpServletRequest request) throws Exception {
    if (multipartFile.isEmpty() == false) {
        //multipartFile.getName()   標簽名字
        //multipartFile.getOriginalFilename()  上傳文件名字
        //multipartFile.getSize()   上傳文件大小
        //multipartFile.getContentType()    上傳文件類型

        //在 webapp 目錄下面(項目目錄下面) 建立一個 resources 資源文件夾, 用來存儲上傳的資源文件
        String parent = request.getServletContext().getRealPath("/resources");
        String filename = UUID.randomUUID() + multipartFile.getOriginalFilename();

        File file = new File(parent, filename);
        multipartFile.transferTo(file);
    }
    return "success";
}

JSP頁面的可變表單請求

<form action="${pageContext.request.contextPath}/upload" 
      enctype="multipart/form-data" 
      method="post">
    <input type="file" name="file" value="請選擇需要上傳的文件" /><br>
    <input type="submit" value="提交">
</form>

4. 原理簡介

DispatcherServlet 初始化的時候,會從容器中加載 MultipartResolver 可變表單解析器,從下面源碼中可以看出加載條件就是 idname 為 multipartResolver 的 bean

接著簡單了解下解析,在 DispatcherServletdoDispatch(..) 方法中檢查該請求是否是可變表單請求,如果是則用加載到緩存的 MultipartResolver 解析器 (這里用到的是注入容器中的 CommonsMultipartResolver 可變表單解析器,其實現了 MultipartResolver 接口) 將可變請求解析成 MultipartFile 對象 (這里是 CommonsMultipartFile,其實現了MultipartFile 接口),放在 HttpServletRequest 對象中,最終通過合適的參數解析器綁定到對應方法的參數上。

十三、文件下載

SpringMVC提供了一個 ResponseEntity 類型,使用它可以很方便地定義返回的 HttpHeadersHttpStatus
以下代碼演示文件的下載功能

@RequestMapping(value = "download", method = RequestMethod.GET)
public ResponseEntity<byte[]> testDownload(HttpServletRequest request, @RequestParam String filename) throws Exception {

    String parent = request.getServletContext().getRealPath("/resources");
    File file = new File(parent, filename);

    byte[] body = FileUtils.readFileToByteArray(file);

    String downloadFilename = new String(file.getName().getBytes("utf-8"), "iso-8859-1");

    HttpHeaders headers = new HttpHeaders();
    //設置文件類型
    headers.add("Content-Disposition", "attchement;filename=" + downloadFilename);

    ResponseEntity responseEntity = new ResponseEntity(body, headers, HttpStatus.OK);
    return responseEntity;
}

十四、攔截器

SpringMVC的處理器攔截器,類似于 Servlet 開發中的過濾器 Filter,用于對處理器進行預處理和后處理。

1. 過濾器與攔截器區別

  • 過濾器:依賴于servlet容器,在實現上基于函數回調,可以對幾乎所有請求進行過濾,但是缺點是一個過濾器實例只能在容器初始化時調用一次。使用過濾器的目的是用來做一些過濾操作,比如:在過濾器中修改字符編碼;在過濾器中修改HttpServletRequest的一些參數,包括:過濾低俗文字、危險字符等。
  • 攔截器:依賴于web框架,在實現上基于Java的反射機制,屬于面向切面編程(AOP)的一種運用。由于攔截器是基于web框架的調用,因此可以使用Spring的依賴注入(DI)進行一些業務操作,同時一個攔截器實例在一個 Controller 生命周期之內可以多次調用。

2. 攔截器接口

攔截器一個有3個回調方法,而一般的過濾器Filter才兩個:

  • preHandle預處理回調方法,實現處理器的預處理。返回值:true表示繼續流程(如調用下一個攔截器或處理器);false表示流程中斷,不會繼續調用其他的攔截器或處理器,此時我們需要通過 response 來產生響應;
  • postHandle后處理回調方法,實現處理器的后處理(但在渲染視圖之前),此時我們可以通過 modelAndView(模型和視圖對象)對模型數據進行處理或對視圖進行處理。
  • afterCompletion整個請求處理完畢回調方法,即在視圖渲染完畢時回調,如性能監控中我們可以在此記錄結束時間并輸出消耗時間,還可以進行一些資源清理,類似于 try-catch-finally 中的finally

3. 代碼編寫

有時候我們可能只需要實現三個回調方法中的某一個,如果實現HandlerInterceptor 接口的話,三個方法必須實現,此時 SpringMVC 提供了一個 HandlerInterceptorAdapter 適配器(一種適配器設計模式的實現),允許我們只實現需要的回調方法,該適配器內部實現了 HandlerInterceptor 接口。

先寫兩個攔截器

public class HandlerInterceptor1 extends HandlerInterceptorAdapter {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("HandlerInterceptor1 preHandle");
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("HandlerInterceptor1 postHandle");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("HandlerInterceptor1 afterCompletion");
    }
}
public class HandlerInterceptor2 extends HandlerInterceptorAdapter {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("HandlerInterceptor2 preHandle");
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("HandlerInterceptor2 postHandle");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("HandlerInterceptor2 afterCompletion");
    }
}

攔截器的注入

<mvc:interceptors>
    <bean class="com.ogemray.interceptor.HandlerInterceptor1"></bean>
    <bean class="com.ogemray.interceptor.HandlerInterceptor2"></bean>
</mvc:interceptors>

Controller 方法編寫

@RequestMapping(value = "/hello")
public String testHello() {
    System.out.println("HelloController.testHello");
    return "success";
}

最終輸出看下執行順序

HandlerInterceptor1 preHandle
HandlerInterceptor2 preHandle
HelloController.testHello
HandlerInterceptor2 postHandle
HandlerInterceptor1 postHandle
HandlerInterceptor2 afterCompletion
HandlerInterceptor1 afterCompletion

4. 運行流程圖

SpringMVC攔截器執行流程.jpg

5. 選擇性攔截注入

有的時候我們需要攔截器攔截指定的請求,這樣也是可以配置的

<mvc:interceptors>
    <mvc:interceptor>
        <!-- 攔截對應 /hello 路徑下的所有請求 -->
        <mvc:mapping path="/hello/*"/>
        <!-- 除去 /hello/test2 這個請求 -->
        <mvc:exclude-mapping path="/hello/test2"></mvc:exclude-mapping>
        <bean class="com.ogemray.interceptor.HandlerInterceptor1"></bean>
    </mvc:interceptor>

    <mvc:interceptor>
        <!-- /* 是一級目錄下的路徑; /** 不分目錄等級, 即所有請求 -->
        <mvc:mapping path="/**"/>
        <bean class="com.ogemray.interceptor.HandlerInterceptor2"></bean>
    </mvc:interceptor>
</mvc:interceptors>

十五、異常處理

在SpringMVC中,所有用于處理在請求映射和請求處理過程中拋出的異常的類,都要實現 HandlerExceptionResolver 接口。
一個基于SpringMVC的Web應用程序中,可以存在多個實現了 HandlerExceptionResolver 的異常處理類,他們的執行順序是由其 order 的值從小到大來先后執行,直到遇到返回的 ModelAndView 不為空則終斷接下來的異常解析器的執行并返回異常的 ModelAndView 對象。

<mvc:annotation-driven />標簽會幫我們注入常用的三個異常解析器:ExceptionHandlerExceptionResolverResponseStatusExceptionResolverDefaultHandlerExceptionResolver

但是我們接下來重點是了解下常用的兩個異常解析器,分別是:ExceptionHandlerExceptionResolverSimpleMappingExceptionResolver

1. ExceptionHandlerExceptionResolver

注意 @ExceptionHandler 注解修飾的方法里面,只能自己 newModelAndView 對象然后裝入需要的注入的值,對于傳參里面帶的 ModelModelMap 達不到傳值要求。

① 異常處理方法寫在對應的類里面

這樣只能處理該 Controller 里面的異常

處理該 Controller 里面所有的異常,在沒有找到指定的異常類對應的處理方法的前提下

@ExceptionHandler
public ModelAndView handlerAllException(Exception e) {
    ModelAndView mv = new ModelAndView();
    mv.addObject("exceptionMsg", e.getMessage());
    mv.setViewName("error");
    System.out.println("HelloController.handlerAllException");
    return mv;
}

處理該 Controller 里面指定類型的異常

@ExceptionHandler(value = {ArithmeticException.class})
public ModelAndView handlerArithmeticException(Exception e) {
    ModelAndView mv = new ModelAndView();
    mv.addObject("exceptionMsg", e.getMessage());
    mv.setViewName("error");
    System.out.println("HelloController.handlerArithmeticException");
    return mv;
}
② 異常處理方法寫在單獨的異常處理類里面

這樣可以處理所有 Controller 的異常,而不是針對單個的 Controller 類,類上需要用 @ControllerAdvice 注解修飾。

@ControllerAdvice
public class HandlerException {
    @ExceptionHandler
    public ModelAndView handlerAllException(Exception e) {
        ModelAndView mv = new ModelAndView();
        mv.addObject("exceptionMsg", e.getMessage());
        mv.setViewName("error");
        System.out.println("HelloController.handlerAllException");
        return mv;
    }
    @ExceptionHandler(value = {ArithmeticException.class})
    public ModelAndView handlerArithmeticException(Exception e) {
        ModelAndView mv = new ModelAndView();
        mv.addObject("exceptionMsg", e.getMessage());
        mv.setViewName("error");
        System.out.println("HelloController.handlerArithmeticException");
        return mv;
    }
}

2. SimpleMappingExceptionResolver

不用自己寫java類處理異常,直接配置就可以了

<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
    <!-- 指定注入異常屬性的key, 默認為 "exception" -->
    <property name="exceptionAttribute" value="ex"></property>
    <property name="exceptionMappings">
        <props>
            <prop key="java.lang.ArrayIndexOutOfBoundsException">error</prop>
        </props>
    </property>
</bean>

十六、整合SpringIOC和SpringMVC

  1. 在 web.xml 中配置 contextLoaderListener,并且加入spring的配置文件 applicationContext.xml
    這樣可以把 service、dao、事務、緩存、以及和其它框架的整合放到 spring 的配置文件里面
    web.xml 文件配置如下
<!DOCTYPE web-app PUBLIC
        "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
        "http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>

    <!-- configure the spring -->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:applicationContext.xml</param-value>
    </context-param>
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    
    <!-- configure the spring mvc -->
    <servlet>
        <servlet-name>dispatcherServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:spring-mvc.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>dispatcherServlet</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

</web-app>
  1. 在 web.xml 中配置 SpringMVC 的 Servlet 和加入 springmvc.xml,這時兩個配置文件中掃描的包有重合的時候出現某些bean會被初始化2次的問題。
    解決:在掃描包的子節點下配置exclude-filterinclude-filter

SpringMVC 只掃描 @Controller@ControllerAdvice

<context:component-scan base-package="com.ogemray.springmvc">
    <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller" />
    <context:include-filter type="annotation" expression="org.springframework.web.bind.annotation.ControllerAdvice" />
</context:component-scan>

Spring排除掃描 @Controller@ControllerAdvice

<context:component-scan base-package="com.ogemray.springmvc">
    <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller" />
    <context:exclude-filter type="annotation" expression="org.springframework.web.bind.annotation.ControllerAdvice" />
</context:component-scan>

注意:Spring 和 SpringMVC 都有一個 IOC 容器,并且Controller 類的 bean 在 SpringMVC 的 IOC 容器中,但是它可以引用 Spring 的 IOC 容器中的 bean 如 service 和 dao 層的 bean,反之則不行,因為 Spring IOC 容器和 SpringMVC IOC 容器是父子關系,相當于全局變量和局部變量的關系!

十七、SpringMVC運行流程

SpringMVC運行流程.jpg

其他相關文章

SpringMVC入門筆記
SpringMVC工作原理之處理映射[HandlerMapping]
SpringMVC工作原理之適配器[HandlerAdapter]
SpringMVC工作原理之參數解析
SpringMVC之自定義參數解析
SpringMVC工作原理之視圖解析及自定義
SpingMVC之<mvc:annotation-driven/>標簽

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容