【SpringMVC從入門到精通】05-RESTful
目錄
RESTful
1、RESTful 簡介
REST
:Representational State Transfer
,表現層資源狀態轉移
-
表現層/表示層
- 資源:Web 工程部署到伺服器上後,當前 Web 工程中的內容在伺服器上都叫“資源”(萬物皆資源)
- 狀態:資源的表現形式,例如,HTML/JSP 頁面、CSS/JS 檔案、圖片/音訊/視訊等皆為資源的“狀態”
- 轉移:瀏覽器傳送請求到伺服器,服務端就將請求的資源“轉移”到客戶端
RESTful
:基於REST
構建的 API 就是RESTful
風格
1.1、資源
- 資源是一種看待伺服器的方式,即將伺服器看作是由很多離散的資源組成。每個資源是伺服器上一個可命名的抽象概念
- 因為資源是一個抽象的概念,所以它不僅僅能代表伺服器檔案系統中的一個檔案、資料庫中的一張表等等具體的東西,也可以將資源設計的要多抽象有多抽象,只要想象力允許而且客戶端應用開發者能夠理解
- 與面向物件設計類似,資源是以名詞為核心來組織的,首先關注的是“名詞”。一個資源可以由一個或多個
URI
來標識。URI
既是資源的名稱,也是資源在 Web 上的地址。對某個資源感興趣的客戶端應用,可以通過資源的URI
與其進行互動
URI
(Uniform Resource Identifier
):統一資源標誌符
URL
(Uniform Resource Locator
):統一資源定位符
URI
是一個抽象的、高層次的概念,而URL
是具體的方式。簡單來說,URL
是一種URI
1.2、資源的表述
- 資源的表述是一段對於資源在某個特定時刻的狀態的描述。可以在客戶端-伺服器端之間轉移(交換)
- 資源的表述可以有多種格式,例如
HTML/XML/JSON/純文字/圖片/視訊/音訊
- 資源的表述格式可以通過協商機制來確定。請求-響應方向的表述通常使用不同的格式
1.3、狀態轉移
- 狀態轉移說的是:在客戶端和伺服器端之間轉移(
transfer
)代表資源狀態的表述 - 通過轉移和操作資源的表述,來間接實現操作資源的目的
2、RESTful 實現
RESTful
的實現,具體說就是:HTTP 協議裡面,四個表示操作方式的動詞GET
、POST
、PUT
、DELETE
它們分別對應四種基本操作:
-
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
表單預設只支援GET
和POST
請求,如果直接通過method
屬性指定為PUT
和DELETE
,會預設以GET
請求方式處理而想要實現
PUT
和DELETE
請求,需要在web.xml
中配置 SpringMVC 提供的HiddenHttpMethodFilter
過濾器,並在前臺頁面使用隱藏域來設定PUT
和DELETE
型別的請求方式當然,也可以使用
AJAX
傳送PUT
和DELETE
請求,但是需要注意PUT
和DELETE
僅部分瀏覽器支援
為了更清楚地瞭解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
配置檔案中對CharacterEncodingFilter
和HiddenHttpMethodFilter
的配置順序如下
<!--處理編碼-->
<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
過濾器儘量配置在其他過濾器之前。這樣就能保證在任何過濾器獲取請求之前,獲得的失已經處理過編碼格式的請求引數了
我們再將CharacterEncodingFilter
和HiddenHttpMethodFilter
的配置順序還原至之前的狀態,即CharacterEncodingFilter
在前而HiddenHttpMethodFilter
在後的情況,進行測試再檢視後臺日誌資訊
修改使用者資訊:User{username='張三', password='', gender='male', age='18', email='[email protected]'}
這時,當請求引數中包含中文時,就不會出現亂碼的情況了
總結
本節重點掌握內容
- 明確
REST
和RESTful
的關係,明確表現層、資源、狀態、轉移這幾個概念的含義-
REST
,表現層資源狀態轉移 -
RESTful
,基於REST
構建的 API 就是RESTful
風格
-
- 明確
RESTful
的實現,是通過不同的請求方式來對應資源的不同操作,通過路徑中的佔位符傳遞請求引數 - 熟練掌握如何通過
RESTful
進行資源的增刪改查操作,以及如何處理PUT
和DELETE
這兩種特殊的請求方式 - 明確
CharacterEncodingFilter
和HiddenHttpMethodFilter
的配置順序,明白兩個過濾器的原始碼處理邏輯
附上導圖,僅供參考