1. 程式人生 > 實用技巧 >[Re] SpringMVC-1

[Re] SpringMVC-1

概述

  • SpringMVC 是 Spring 的 Web 模組。
  • Spring 為展現層提供的基於 MVC 設計理念的優秀的 Web 框架,是目前最主流的 MVC 框架之一。
  • Spring3.0 後全面超越 Struts2,成為最優秀的 MVC 框架。
  • Spring MVC 通過一套 MVC 註解,讓 POJO(Plain Old Java Object) 成為處理請求的控制器,而無須實現任何介面。
  • 支援 REST 風格的 URL 請求。
  • 採用了鬆散耦合可插拔元件結構,比其他 MVC 框架更具擴充套件性和靈活性。

HelloWorld

步驟

  • 建立 Web 工程
  • 導包
    • SpringMVC 是 Spring 的 Web 模組;所有模組的執行都是依賴核心模組[IOC 模組]
      commons-logging-1.1.3.jar // 日誌
      spring-aop-4.0.0.RELEASE.jar // 支援註解
      // 核心 ↓
      spring-beans-4.0.0.RELEASE.jar
      spring-context-4.0.0.RELEASE.jar
      spring-core-4.0.0.RELEASE.jar
      spring-expression-4.0.0.RELEASE.jar
      
    • Web 模組
      spring-web-4.0.0.RELEASE.jar
      spring-webmvc-4.0.0.RELEASE.jar
      
  • 寫配置檔案
    • web.xml
      <!--
          SpringMVC 思想:有一個前端控制器能攔截所有請求,並智慧派發
          這個前端控制器是一個 Servlet,應該在 web.xml 中配置這個 Servlet 來攔截所有請求
       -->
      
      <!-- The front controller of this Spring Web application,
              responsible for handling all application requests -->
      <servlet>
          <servlet-name>springDispatcherServlet</servlet-name>
          <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
          <init-param>
              <!-- contextConfigLocation 指定 SpringMVC 配置檔案的位置 -->
              <param-name>contextConfigLocation</param-name>
              <param-value>classpath:springMVC.xml</param-value>
          </init-param>
          <!-- 
              Servlet 啟動載入,原本是在第 1 次訪問建立物件。設定該子標籤後,
              Servlet 會在 Server 啟動時建立物件。值越小,優先順序越高,越先建立物件
           -->
          <load-on-startup>1</load-on-startup>
      </servlet>
      
      <!-- Map all requests to the DispatcherServlet for handling -->
      <servlet-mapping>
          <servlet-name>springDispatcherServlet</servlet-name>
          <!-- 
              '/*' 和 '/' 都是攔截所有請求。
              但是 '/*' 範圍更廣,會攔截到 *.jsp 頁面,一旦攔截,jsp 頁面就無法顯示。
              '/' 也會攔截所有請求,但不會攔截 jsp。
           -->
          <url-pattern>/</url-pattern>
      </servlet-mapping>
      
    • springMVC.xml (建立 Spring Bean Configuration File,即 Web 層的 Spring 容器)
      <!-- 掃描元件 -->
      <context:component-scan base-package="cn.edu.nuist"></context:component-scan>
      
      <!-- 配置一個檢視解析器,能幫我們拼接頁面地址 -->
      <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
          <property name="prefix" value="/WEB-INF/pages/"></property>
          <property name="suffix" value=".jsp"></property>
      </bean>
      
  • 編寫測試程式碼
    • index.jsp
      <a href="hello">HelloWorld!</a>
      
    • success.jsp
      <body>SUCCESS!</body>
      
    • MyFirstController
      @Controller // 告訴 SpringMVC 這是一個處理器,可以處理請求
      public class MyFirstController {
          /*
           * '/' 代表從當前專案下開始,處理當前專案下的 hello 請求
           */
          @RequestMapping("/hello")
          public String myFirstRequest() {
              System.out.println("請求收到了,正在處理中...");
              /*
               * [檢視解析器] 會自動拼串:
               * 	<property name="prefix" value="/WEB-INF/pages/">
               * 	<property name="suffix" value=".jsp">
               * → /WEB-INF/pages/[方法返回值].jsp
               */
              // return "/WEB-INF/pages/success.jsp";
              return "success";
          }
      }
      

細節

執行流程

  1. 客戶端點選超連結會發送 http://localhost:8080/21_SpringMVC/hello 請求
  2. 來到 tomcat 伺服器,SpringMVC 的 [前端控制器] 收到所有請求
  3. 來看請求地址和 @RequestMapping 標註的哪個匹配,來找使用哪個類的哪個方法來處理請求
  4. [前端控制器] 找到了 {目標處理器類&目標處理方法},直接利用反射執行目標方法
  5. 方法執行完成以後會有一個返回值:SpringMVC 認為這個返回值就是要去的頁面地址
  6. 拿到方法返回值後,用 [檢視解析器] 拼串得到完整的頁面地址
  7. 拿到頁面地址後,[前端控制器] 幫我們轉發到指定頁面

@RequestMapping 簡述

告訴 SpringMVC 這個方法用來處理什麼請求。
@RequestMapping("/hello") 這個 '/' 可以省略,即使省略,也是預設從當前專案下開始。不過習慣加上。

配置檔案的預設位置

可以看出,如果不指定配置檔案位置,SpringMVC 預設會去找如下檔案:

所以,若不想指定配置檔案位置,就在 web 應用的 WEB-INF 目錄下,指定配置檔名為 <servlet-name>-servlet.xml 即可。

2.2.4 核心控制器的 url-pattern

  • Tomcat Server 的 web.xml 中有一個 DefaultServlet,其 url-pattern 的配置是 /
    • The default servlet for all web applications, that serves static resources.
    • DefaultServlet 就負責找到並返回靜態資源
  • [前端控制器] 的 url-pattern 也配置為了 /,這就相當於把 DefaultServlet 禁用了。此時,若再去訪問一個靜態資源(除去 Jsp 和 Servlet 外,其餘都是靜態資源)時,就會讓 [前端控制器] 來處理該請求,而 [前端控制器] 就會去比較哪個方法的 @RequestMapping 與請求對應,肯定找不到,所以最終返回 404。
  • 為什麼 Jsp 能訪問?
    • 伺服器的 web.xml 中還有一個 JspServlet,其 url-pattern 配置為 *.jsp
      The JSP page compiler and execution servlet, which is the mechanism
      used by Tomcat to support JSP pages. Traditionally, this servlet is
      mapped to the URL pattern "*.jsp".
      
    • 因為我們專案自身 web.xml 中並沒有覆蓋伺服器的 JspServlet 的配置;所以,JspServlet 能正常處理 Jsp 請求。
  • 當把 [前端控制器] 的 url-pattern 配置為了 /*,這時,連 Jsp 也訪問不到了。
  • 我們把專案的 url-pattern 配置為 / 也是為了迎合後來的 REST 風格的 URL 地址。

@RequestMapping

Spring MVC 使用 @RequestMapping 註解為控制器指定可以處理哪些 URL 請求。

註解標註在類上

在控制器的類定義及方法定義處都可標註 @RequestMapping。

  • 類定義處:提供初步的請求對映資訊。相對於 WEB 應用的根目錄
  • 方法處:提供進一步的細分對映資訊。相對於類定義處的 URL。若類定義處未標註 @RequestMapping,則方法處標記的 URL 相對於 WEB 應用的根目錄

註解中的屬性

標準的 HTTP 請求報頭:

屬性之間是的關係,聯合使用多個條件可讓請求對映更加精確化。

對映請求路徑

DispatcherServlet 截獲請求後,就通過將請求 URL 與控制器上 @RequestMapping 註解的 value 屬性值進行比對以此確定請求所對應的處理方法。

對映請求方法

public enum RequestMethod {
    GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE
}

若不是按規定的方式請求:405 - Request method 'GET' not supported

對映請求引數

params 規定請求攜帶的請求引數,支援簡單的表示式 (不按要求的直接 404),引數規則間用 , 隔開。

params={"username"} 請求必須攜帶名為 username 的引數
params={"!username"} 請求不能攜帶名為 username 的引數
params={"username=admin"} 請求必須攜帶名為 username 且值為 admin 的引數
params={"username!=admin"} 請求必須攜帶名為 username 但值不能是 admin 的引數

舉例:{"param1=value1", "param2"} 請求必須包含名為 param1 和 param2 的兩個請求引數,且 param1 引數的值必須為 value1。

對映請求頭

headers 規定請求頭,使用方式和 params 類似 (不按要求的也是直接 404)。

Ant 風格的 URL

  • Ant 風格資源地址支援 3 種匹配符:
     ?:匹配檔名中的一個字元(0個/多個都不行)
     *:匹配檔名中的任意字元或者一層路徑(0層/多層都不行)
    **:匹配多層路徑
    
  • @RequestMapping 還支援 Ant 風格的 URL
    /user/*/createUser: 匹配 /user/aaa/createUser、/user/bbb/createUser 等 URL
    /user/**/createUser: 匹配 /user/createUser、/user/aaa/bbb/createUser 等 URL
    /user/createUser??: 匹配 /user/createUseraa、/user/createUserbb 等 URL
    

@PathVariable

帶佔位符的 URL 是 Spring3.0 新增的功能,該功能在 SpringMVC 向 REST 目標挺進發展過程中具有里程碑的意義。

通過 @PathVariable 可以將 URL 中佔位符引數繫結到控制器處理方法的形參中:URL 中的 {xxx} 佔位符可以通過 @PathVariable("xxx") 繫結到操作方法的形參中。

REST

REST 風格

  • 簡述
  • 示例(URL 起名方式:/資源名/資源識別符號
  • 【總結】URL 定位資源,用 HTTP 動詞(GET,POST,DELETE,PUT)描述操作。
    • 看 URL 就知道要什麼
    • 看 HTTP Method 就知道幹什麼
    • 看 HTTP StatusCode 就知道結果如何

HiddenHttpMethodFilter

瀏覽器 form 表單只支援 GET 與 POST 請求,而DELETE、PUT 等 method 並不支援,Spring3.0 添加了這個過濾器,可以將這些請求轉換為標準的 HTTP 方法,使得支援 GET、POST、PUT 與 DELETE 請求。

public class HiddenHttpMethodFilter extends OncePerRequestFilter {

    public static final String DEFAULT_METHOD_PARAM = "_method";

    private String methodParam = DEFAULT_METHOD_PARAM;

    public void setMethodParam(String methodParam) {
        Assert.hasText(methodParam, "'methodParam' must not be empty");
        this.methodParam = methodParam;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response
            , FilterChain filterChain) throws ServletException, IOException {
        String paramValue = request.getParameter(this.methodParam);
        if ("POST".equals(request.getMethod()) && StringUtils.hasLength(paramValue)) {
            String method = paramValue.toUpperCase(Locale.ENGLISH);
            HttpServletRequest wrapper = new HttpMethodRequestWrapper(request, method);
            filterChain.doFilter(wrapper, response);
        } else {
            filterChain.doFilter(request, response);
        }
    }

    private static class HttpMethodRequestWrapper extends HttpServletRequestWrapper {

        private final String method;

        public HttpMethodRequestWrapper(HttpServletRequest request, String method) {
            super(request);
            this.method = method;
        }

        @Override
        public String getMethod() {
            return this.method;
        }
    }
}

請求處理

  • Spring MVC 通過分析處理方法的簽名,HTTP 請求資訊繫結到處理方法的相應形參中。
  • Spring MVC 對控制器處理方法簽名的限制是很寬鬆的,幾乎可以按喜歡的任何方式對方法進行簽名。
  • 必要時可以對方法及方法形參標註相應的註解(@PathVariable 、@RequestParam、@RequestHeader 等)
  • Spring MVC 框架會將 HTTP 請求的資訊繫結到相應的方法形參中,並根據方法的返回值型別做出相應的後續處理

@RequestParam

  • 在處理方法形參處使用 @RequestParam 可以把請求引數傳遞給請求方法
  • 註解屬性
    • value 引數名
    • required 是否必須。預設為 true,表示請求引數中必須包含對應的引數;若不存在,將丟擲異常
    • defaultValue 預設值,當沒有傳遞引數時使用該值

@RequestHeader

  • 使用 @RequestHeader 繫結請求報頭的屬性值
  • 請求頭包含了若干個屬性,伺服器可據此獲知客戶端的資訊,通過 @RequestHeader 即可將請求頭中的屬性值繫結到處理方法的形參中
  • 註解屬性同 @RequestParam

@CookieValue

使用 @CookieValue 繫結請求中的 Cookie 值到處理方法的某個形參。

POJO 作為引數

使用 POJO 物件繫結請求引數值時,Spring MVC 會按請求引數名和 POJO 屬性名進行自動匹配,自動為該物件填充屬性值。支援級聯屬性。如:dept.deptId、dept.address.tel 等。

ServletAPI 作為引數

MVC 的 Handler 方法可以接受如下 ServletAPI 型別的引數(原始碼參考:AnnotationMethodHandlerAdapter)

  1. HttpServletRequest
  2. HttpServletResponse
  3. HttpSession
  4. java.security.Principal
  5. Locale
  6. InputStream
  7. OutputStream
  8. Reader
  9. Writer

亂碼問題

GET 亂碼解決

POST 亂碼解決

配置字元編碼過濾器。但要注意:當有多個 Filter 時,一般都會將字元編碼 Filter 置首!因為 CharacterEncodingFilter 必須要在第一次獲取請求引數之前設定才有意義。

假設現在配了倆過濾器:HiddenHttpMethodFilter 和 CharacterEncodingFilter。如果先配置了 HiddenHttpMethodFilter,而其 doFilter() 中呼叫的 request.getParameter(this.methodParam) 會使得字元編碼過濾器失效。所以,一般在 web.xml 中都會先配置字元編碼過濾器。

public class CharacterEncodingFilter extends OncePerRequestFilter {

    private String encoding;

    private boolean forceEncoding = false;

    public void setEncoding(String encoding) {
        this.encoding = encoding;
    }

    public void setForceEncoding(boolean forceEncoding) {
        this.forceEncoding = forceEncoding;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
            response, FilterChain filterChain) throws ServletException, IOException {
        if (this.encoding != null && (this.forceEncoding 
                || request.getCharacterEncoding() == null)) {
            // 設定請求編碼
            request.setCharacterEncoding(this.encoding);
            if (this.forceEncoding) {
                // 設定響應編碼
                response.setCharacterEncoding(this.encoding);
            }
        }
        filterChain.doFilter(request, response);
    }
}