1. 程式人生 > 其它 >【SpringMVC從入門到精通】05-RESTful

【SpringMVC從入門到精通】05-RESTful

筆記來源:【尚矽谷】SpringMVC教程丨一套快速上手spring mvc

目錄

RESTful

1、RESTful 簡介

RESTRepresentational State Transfer,表現層資源狀態轉移

  • 表現層/表示層
    :前端的檢視頁面到後端的控制層即為“表現層”
  • 資源:Web 工程部署到伺服器上後,當前 Web 工程中的內容在伺服器上都叫“資源”(萬物皆資源)
  • 狀態:資源的表現形式,例如,HTML/JSP 頁面、CSS/JS 檔案、圖片/音訊/視訊等皆為資源的“狀態”
  • 轉移:瀏覽器傳送請求到伺服器,服務端就將請求的資源“轉移”到客戶端

RESTful:基於REST構建的 API 就是RESTful風格

1.1、資源

  • 資源是一種看待伺服器的方式,即將伺服器看作是由很多離散的資源組成。每個資源是伺服器上一個可命名的抽象概念
  • 因為資源是一個抽象的概念,所以它不僅僅能代表伺服器檔案系統中的一個檔案、資料庫中的一張表等等具體的東西,也可以將資源設計的要多抽象有多抽象,只要想象力允許而且客戶端應用開發者能夠理解
  • 與面向物件設計類似,資源是以名詞為核心來組織的,首先關注的是“名詞”。一個資源可以由一個或多個URI來標識。URI既是資源的名稱,也是資源在 Web 上的地址。對某個資源感興趣的客戶端應用,可以通過資源的URI與其進行互動

URIUniform Resource Identifier):統一資源標誌符

URLUniform Resource Locator):統一資源定位符

URI是一個抽象的、高層次的概念,而URL是具體的方式。簡單來說,URL是一種URI

1.2、資源的表述

  • 資源的表述是一段對於資源在某個特定時刻的狀態的描述。可以在客戶端-伺服器端之間轉移(交換)
  • 資源的表述可以有多種格式,例如HTML/XML/JSON/純文字/圖片/視訊/音訊
    等等
  • 資源的表述格式可以通過協商機制來確定。請求-響應方向的表述通常使用不同的格式

1.3、狀態轉移

  • 狀態轉移說的是:在客戶端和伺服器端之間轉移(transfer)代表資源狀態的表述
  • 通過轉移和操作資源的表述,來間接實現操作資源的目的

2、RESTful 實現

RESTful的實現,具體說就是:HTTP 協議裡面,四個表示操作方式的動詞GETPOSTPUTDELETE

它們分別對應四種基本操作:

  • GET用來獲取資源
  • POST用來新建資源
  • PUT用來更新資源
  • DELETE用來刪除資源

REST風格URL地址不使用問號鍵值對方式攜帶請求引數,而是:

提倡使用統一的風格設計,從前到後各個單詞使用斜槓分開,將要傳送給伺服器的資料作為URL地址的一部分,以保證整體風格的一致性

以往,我們訪問資源的方式五花八問。例如,

  • 獲取使用者資訊通過getUserById/selectUserById/findUserById/等
  • 刪除使用者資訊通過deleteUserById/removeUserById
  • 更新使用者資訊通過updateUser/modifyUser/saveUser
  • 新增使用者資訊通過addUser/createUser/insertUser

上述操作的資源都是使用者資訊。按照RESTful思想,既然操作的資源一樣,那麼請求路徑就應該一樣

用一張表格來對比傳統方式和REST風格對資源操作的區別

操作 傳統方式 REST 風格
查詢 getUserById?id=1 user/1-->get請求
儲存 saveUser user-->post請求
刪除 deleteUserById?id=1 user/1-->delete請求
更新 updateUser user-->put請求

3、使用 RESTful 模擬操作使用者資源

需求分析:使用 RESTful 模擬使用者資源的增刪改查,通過路徑中的佔位符傳遞請求引數,通過不同的請求方式對應資源的不同操作

RESTful 路徑 請求方式 操作
/user GET 查詢所有使用者資訊
/user/1 GET 根據使用者ID查詢使用者資訊
/user POST 新增使用者資訊
/user/1 DELETE 根據使用者ID刪除使用者資訊
/user PUT 修改使用者資訊

3.1、使用者的查詢、新增

後臺程式碼實現

RESTfulController.java

@Controller
@RequestMapping("restfulcontroller")
public class RESTfulController {
    @RequestMapping(value = "/user", method = RequestMethod.GET)
    public String getAllUser() {
        System.out.println("查詢所有使用者資訊");
        return "success";
    }

    @RequestMapping(value = "/user/{id}", method = RequestMethod.GET)
    public String getUserById(@PathVariable("id") String id) {
        System.out.println("根據使用者ID查詢使用者資訊:" + id);
        return "success";
    }

    @RequestMapping(value = "/user", method = RequestMethod.POST)
    public String insertUser(User user) { // User 沿用之前的物件
        System.out.println("新增使用者資訊:" + user);
        return "success";
    }
    
    /** 注:由於 PUT、DELETE 請求比較特殊,後面再做補充 */
}

前臺測試程式碼

restful.html


<a th:href="@{/restfulcontroller/user}">查詢所有使用者資訊</a><br/>
<a th:href="@{/restfulcontroller/user/1}">根據使用者ID查詢使用者資訊</a><br/>
<form th:action="@{/restfulcontroller/user}" method="post">
    使用者名稱:<input type="text" name="username"><br/>
    密碼:<input type="password" name="password"><br/>
    性別:<input type="radio" name="gender" value="male">男<input type="radio" name="gender" value="female">女<br/>
    年齡:<input type="number" name="age"><br/>
    郵箱:<input type="text" name="email"><br/>
    <input type="submit" value="新增使用者">
</form>

為了能夠訪問到restful.html前臺頁面資源,可以通過在控制器中定義一個控制器方法來返回其檢視,也可以通過在04-SpringMVC 檢視中提到的view-controller檢視控制器代替。因為這裡只是為了實現頁面跳轉,沒有其他請求過程的處理,所以可以通過在 SpringMVC 配置檔案中使用<view-controller>標籤進行設定

<mvc:view-controller path="/restful" view-name="restful"></mvc:view-controller>

測試結果

後臺日誌資訊

查詢所有使用者資訊
根據使用者ID查詢使用者資訊:1
新增使用者資訊:User{username='admin', password='11111', gender='male', age='18', email='[email protected]'}

3.2、使用者的修改、刪除

01-@RequestMapping 註解中,我們提到過form表單預設只支援GETPOST請求,如果直接通過method屬性指定為PUTDELETE,會預設以GET請求方式處理

而想要實現PUTDELETE請求,需要在web.xml中配置 SpringMVC 提供的HiddenHttpMethodFilter過濾器,並在前臺頁面使用隱藏域來設定PUTDELETE型別的請求方式

當然,也可以使用AJAX傳送PUTDELETE請求,但是需要注意PUTDELETE僅部分瀏覽器支援

為了更清楚地瞭解HiddenHttpMethodFilter過濾器到底幹了什麼,這裡對其原始碼進行剖析

public class HiddenHttpMethodFilter extends OncePerRequestFilter {
    private static final List<String> ALLOWED_METHODS = Collections.unmodifiableList(Arrays.asList(HttpMethod.PUT.name(),HttpMethod.DELETE.name(),HttpMethod.PATCH.name()));
    public static final String DEFAULT_METHOD_PARAM = "_method";
    private String methodParam = DEFAULT_METHOD_PARAM;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
        throws ServletException, IOException {
        HttpServletRequest requestToUse = request;
        // 要求原請求方式為 POST 請求
        if ("POST".equals(request.getMethod()) && request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) == null) {
            // methodParam值:_method
            // paramValue值:_method屬性值
            String paramValue = request.getParameter(this.methodParam);
            if (StringUtils.hasLength(paramValue)) {
                // _method屬性值轉為全大寫形式,put==>PUT,delete==>DELETE
                String method = paramValue.toUpperCase(Locale.ENGLISH);
                // 判斷_method屬性值是否為{"PUT","DELETE","PATCH"}中的一個
                if (ALLOWED_METHODS.contains(method)) {
                    // “偷樑換柱”,包裝為一個新的請求物件
                    requestToUse = new HttpMethodRequestWrapper(request, method);
                }
            }
        }
        filterChain.doFilter(requestToUse, 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;
        }
    }
}

“原始碼在手,天下我有”。接下來,將理論付諸實踐

配置檔案

web.xml配置HiddenHttpMethodFilter過濾器

<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>

後臺程式碼實現

@RequestMapping(value = "/user", method = RequestMethod.PUT)
public String updateUser(User user) {
    System.out.println("修改使用者資訊:" + user);
    return "success";
}
// 這裡的寫法格式沒有問題,但業務邏輯其實大有問題,下面再詳細說
@RequestMapping(value = "/user", method = RequestMethod.DELETE)
public String deleteUser(User user) {
    System.out.println("刪除使用者資訊:" + user);
    return "success";
}

前臺測試程式碼

在新增使用者的表單基礎上,新增隱藏域<input type="hidden" name="_method" value="put">

<form th:action="@{/restfulcontroller/user}" method="post">
    <input type="hidden" name="_method" value="put">
    使用者名稱:<input type="text" name="username"><br/>
    密碼:<input type="password" name="password"><br/>
    性別:<input type="radio" name="gender" value="male">男<input type="radio" name="gender" value="female">女<br/>
    年齡:<input type="number" name="age"><br/>
    郵箱:<input type="text" name="email"><br/>
    <input type="submit" value="修改使用者">
</form>
<hr/>
<!-- 這裡的寫法格式沒有問題,但業務邏輯其實大有問題,下面再詳細說 -->
<form th:action="@{/restfulcontroller/user}" method="post">
    <input type="hidden" name="_method" value="delete">
    使用者名稱:<input type="text" name="username"><br/>
    密碼:<input type="password" name="password"><br/>
    性別:<input type="radio" name="gender" value="male">男<input type="radio" name="gender" value="female">女<br/>
    年齡:<input type="number" name="age"><br/>
    郵箱:<input type="text" name="email"><br/>
    <input type="submit" value="刪除使用者">
</form>

測試結果

修改使用者

刪除使用者

後臺日誌資訊

修改使用者資訊:User{username='user', password='11111', gender='female', age='1', email='[email protected]'}
刪除使用者資訊:User{username='user', password='11111', gender='female', age='1', email='[email protected]'}

到這裡,應該需要指明的是我們在設計RESTful實現時,對刪除使用者資訊的要求下面這樣的

RESTful 路徑 請求方式 操作
/user/1 DELETE 根據使用者ID刪除使用者資訊

換句話說,這裡應該通過超連結而非表單形式,即通過使用者ID來對使用者資訊進行刪除操作。而一般情況下,我們是通過行編輯刪除某一行資料,或是通過選中表單的資料來進行批量刪除,這些功能在詳細案例時會詳細介紹

4、CharacterEncodingFilter 和 HiddenHttpMethodFilter 的配置順序

目前,我們在web.xml配置檔案中對CharacterEncodingFilterHiddenHttpMethodFilter的配置順序如下

<!--處理編碼-->
<filter>
    <filter-name>characterEncodingFilter</filter-name>
    <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
    <init-param>
        <param-name>encoding</param-name>
        <param-value>UTF-8</param-value>
    </init-param>
    <init-param>
        <param-name>forceResponseEncoding</param-name>
        <param-value>true</param-value>
    </init-param>
</filter>
<filter-mapping>
    <filter-name>characterEncodingFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
<!-- 略 -->
<!--配置 org.springframework.web.filter.HiddenHttpMethodFilter: 可以把 POST 請求轉為 DELETE 或 POST 請求 -->
<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>

如果,將兩者順序顛倒互換,即

<!--配置 org.springframework.web.filter.HiddenHttpMethodFilter: 可以把 POST 請求轉為 DELETE 或 POST 請求 -->
<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>
<!-- 略 -->
<!--處理編碼-->
<filter>
    <filter-name>characterEncodingFilter</filter-name>
    <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
    <init-param>
        <param-name>encoding</param-name>
        <param-value>UTF-8</param-value>
    </init-param>
    <init-param>
        <param-name>forceResponseEncoding</param-name>
        <param-value>true</param-value>
    </init-param>
</filter>
<filter-mapping>
    <filter-name>characterEncodingFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

看看順序改變之後,對請求的編碼會有什麼影響

後臺日誌資訊

修改使用者資訊:User{username='张三', password='123456', gender='male', age='18', email='[email protected]'}

可以發現,中文的“張三”亂碼了,這是為什麼呢?

02-SpringMVC 獲取請求引數一節中的7、處理亂碼問題中,我們嘗試在獲取請求引數之前,通過request.setCharacterEncoding("UTF-8");來設定請求編碼格式,但是沒有生效。分析的原因是“請求引數獲取在前,設定編碼格式在後”導致的。

我們也提出了 2 種解決方案:

  • 1、獲取請求引數之後,手動解碼編碼。但是這種顯然不合理,所以直接 pass
  • 2、獲取請求引數之前“做手腳”。這種方式就是 SpringMVC 中提供的CharacterEncodingFilter過濾器,來對請求編碼做統一處理

現在的問題就是:在CharacterEncodingFilter之前配置了HiddenHttpMethodFilter導致了失效

所以我們需要搞清楚,為什麼CharacterEncodingFilter的配置順序會影響到編碼的效果?或者說為什麼HiddenHttpMethodFilter會使之失效?

通過上面對HiddenHttpMethodFilter原始碼的剖析,它會獲得_method這個請求引數,這就導致執行到CharacterEncodingFilter過濾器時,已經是獲取請求引數之後了,所以會導致上述中文亂碼問題

因此,我們必須要將CharacterEncodingFilter過濾器儘量配置在其他過濾器之前。這樣就能保證在任何過濾器獲取請求之前,獲得的失已經處理過編碼格式的請求引數了

我們再將CharacterEncodingFilterHiddenHttpMethodFilter的配置順序還原至之前的狀態,即CharacterEncodingFilter在前而HiddenHttpMethodFilter在後的情況,進行測試再檢視後臺日誌資訊

修改使用者資訊:User{username='張三', password='', gender='male', age='18', email='[email protected]'}

這時,當請求引數中包含中文時,就不會出現亂碼的情況了

總結

本節重點掌握內容

  • 明確RESTRESTful的關係,明確表現層、資源、狀態、轉移這幾個概念的含義
    • REST,表現層資源狀態轉移
    • RESTful,基於REST構建的 API 就是RESTful風格
  • 明確RESTful的實現,是通過不同的請求方式來對應資源的不同操作,通過路徑中的佔位符傳遞請求引數
  • 熟練掌握如何通過RESTful進行資源的增刪改查操作,以及如何處理PUTDELETE這兩種特殊的請求方式
  • 明確CharacterEncodingFilterHiddenHttpMethodFilter的配置順序,明白兩個過濾器的原始碼處理邏輯

附上導圖,僅供參考