歡迎訪問筆者個人技術博客:http://rukihuang.xyz/
一、REST和RESTful
1.1 Resource
- 所謂"資源",就是網絡上的一個實體,或者說是網絡上的一個具體信息。它可以是一段文本、一張圖片、一首歌曲、一種服務,總之就是一個具體的實在。你可以用一個URI(統一資源標識符)指向它,每種資源對應一個特定的URI。要獲取這個資源,訪問它的URI就可以,因此URI就成了每一個資源的地址或獨一無二的識別符。(URL是URI的子集,即具體實現)
http://rukihuang.xyz/index.php/archives/92/
1.2 Representation
- “資源"是一種信息實體,它可以有多種外在表現形式。我們把"資源"具體呈現出來的形式,叫做它的"表現層”(Representation)。
- 比如,文本可以用txt格式表現,也可以用HTML格式、XML格式、JSON格式表現,甚至可以採用二進制格式;圖片可以用JPG格式表現,也可以用PNG格式表現。
- URI只代表資源的實體,不代表它的形式。它的具體表現形式,應該在HTTP請求的頭信息中用
Accept
和Content-Type
字段指定,這兩個字段纔是對"表現層"的描述accept:application/json
content-type:application/json
1.3 State Transfer
- 訪問一個網站,就代表了客戶端和服務器的一個互動過程。在這個過程中,勢必涉及到數據和狀態的變化。
- 互聯網通信協議HTTP協議,是一個無狀態協議。這意味着,所有的狀態都保存在服務器端。因此,如果客戶端想要操作服務器,必須通過某種手段,讓服務器端發生"狀態轉化"(State Transfer)。而這種轉化是建立在表現層之上的,所以就是"表現層狀態轉化"。
- 改變:(服務器端)資源的狀態
- 新增:從無到有狀態的變化
- 更新:從某個狀態變成另外一種狀態的轉化
- 刪除:從有到無狀態的變化
1.4 Uniform Interface
-
統一接口:REST要求,必須通過統一的接口來對資源執行各種操作。對於每個資源只能執行一組有限的操作。
-
HTTP1.1協議爲例:
- 7個HTTP方法:
GET
/POST
/PUT
/DELETE
/PATCH
/HEAD
/OPTIONS
- HTTP頭信息(可自定義)
- HTTP響應狀態代碼(可自定義)
- 7個HTTP方法:
-
REST還要求,對於資源執行的操作,其操作語義必須由HTTP消息體之前的部分完全表達,不能將操作語義封裝在HTTP消息體內部。
- 以前:
localhost:8080/employee/saveOrUpdate.do?id=1&name=xx
- 現在:
localhost:8080/employees
- 以前:
二、關於應用接口
- 系統的接口:很多情況下,需要把系統的功能作爲服務暴露給外部的其他應用使用或者給移動端使用,就需要把系統中的服務作爲接口暴露出去,一般分爲公共接口(發短信,天氣服務)和私用接口(公司內部使用的)。
三、RESTful設計
3.1 資源設計
3.1.1 路徑
-
使用以前的方式完成需求:
- 1,帖子列表,支持過濾和分頁;
http://bbs.example.cn/bbs_list.do?keyword=xx¤tPage=1
- 2,查看一篇帖子;
http://bbs.example.cn/bbs_view.do?id=xx
- 3,查看一篇帖子的所有回帖;
http://bbs.example.cn/bbs_replay_list.do?id=xx
- 4,回帖;
http://bbs.example.cn/bbs_replay.do?id=xx&content=xxx
- 5,發佈一篇帖子;
http://bbs.example.cn/bbs_post.do?title=xx&content=xxx
- 6,修改一篇帖子;
http://bbs.example.cn/bbs_update.do?title=xx&content=xxx&id=xx
- 7,刪除一篇帖子;
http://bbs.example.cn/bbs_delete.do?id=xx
- 這種方式的問題:大量的接口方法,URL地址設計複雜,需要在URL裏面表示出資源及其操作;路徑又稱"終點"(endpoint),表示API的具體網址。
- 1,帖子列表,支持過濾和分頁;
-
在RESTful架構中,每個網址代表一種資源(resource),所以網址中不能有動詞,只能有名詞,而且所用的名詞往往與數據庫的表格名對應。一般來說,數據庫中的表都是同種記錄的"集合"(collection),所以API中的名詞也應該使用複數。
https://api.example.com/v1/zoos:動物園資源
https://api.example.com/v1/animals:動物資源
https://api.example.com/v1/employees:飼養員資源
-
參考例子
https://api.github.com/
http://docs.jiguang.cn/jmessage/server/rest_api_im/
-
總結:
- 每個url代表一種資源. uri上不能有動詞, 只能是名詞,並且一般都和數據庫的表名相同,並且是使用複數
/sessions post
/sessions delete
- 上傳圖片的接口
/images post
/images delete
- 每個url代表一種資源. uri上不能有動詞, 只能是名詞,並且一般都和數據庫的表名相同,並且是使用複數
3.2 動作設計
3.2.1 HTTP動作
GET(SELECT)
:從服務器取出資源(一項或多項)。POST(CREATE)
:在服務器新建一個資源。PUT(UPDATE)
:在服務器更新資源(客戶端提供改變後的完整資源)。PATCH(UPDATE)
:在服務器更新資源(客戶端提供改變的屬性【補丁】)。DELETE(DELETE)
:從服務器刪除資源。HEAD
:獲得一個資源的元數據,比如一個資源的hash值或者最後修改日期;OPTIONS
:獲得客戶端針對一個資源能夠實施的操作;(獲取該資源的api(能夠對資源做什麼操作的描述))
3.2.2 動作示例
-
GET /zoos
:列出所有動物園 -
POST /zoos
:新建一個動物園 -
GET /zoos/ID
:獲取某個指定動物園的信息 -
PUT /zoos/ID
:更新某個指定動物園的信息(提供該動物園的全部信息) -
PATCH /zoos/ID
:更新某個指定動物園的信息(提供該動物園的部分信息) -
DELETE /zoos/ID
:刪除某個動物園 -
GET /zoos/ID/animals
:列出某個指定動物園的所有動物/zoos/1/animals
獲取某個部門的所有員工
3.3 返回結果
3.3.1 返回值類型
GET /zoos
:返回資源對象的列表(數組/集合)GET /zoos/1
:返回單個資源對象POST /collection
:返回新生成的資源對象PUT /collection/resource
:返回完整的資源對象PATCH /collection/resource
:返回完整的資源對象DELETE /collection/resource
:返回一個空文檔
3.3.2 常見狀態碼
狀態碼 | 描述 | 備註 |
---|---|---|
200 | OK - [GET] | 服務器成功返回用戶請求的數據。 |
201 | CREATED - [POST/PUT/PATCH] | 用戶新建或修改數據成功。 |
202 | Accepted - [*] | 表示一個請求已經進入後臺排隊(異步任務) |
204 | NO CONTENT - [DELETE] | 用戶刪除數據成功。 |
400 | INVALID REQUEST - [POST/PUT/PATCH] | 用戶發出的請求有錯誤,服務器沒有進行新建或修改數據的操作,該操作是冪等的。 |
401 | Unauthorized - [*] | 表示用戶沒有權限(令牌、用戶名、密碼錯誤)。 |
403 | Forbidden - [*] | 表示用戶得到授權(與401錯誤相對),但是訪問是被禁止的。 |
404 | NOT FOUND - [*] | 用戶發出的請求針對的是不存在的記錄,服務器沒有進行操作,該操作是冪等的。 |
406 | Not Acceptable - [GET] | 用戶請求的格式不可得(比如用戶請求JSON格式,但是隻有XML格式)。 |
410 | Gone -[GET] | 用戶請求的資源被永久刪除,且不會再得到的。 |
422 | Unprocesable entity - [POST/PUT/PATCH] | 當創建一個對象時,發生一個驗證錯誤。 |
3.3.3 Content-Type
-
一個API可以允許返回JSON,Xml甚至HTML等文檔格式;建議使用json;
-
以前通過URL來規定獲取得格式類型,比如:
https://api.example.com/employee.json
https://api.example.com/employee.html
-
但是更建議使用Accept這個請求頭;
-
Accept與Content-Type的區別:
-
Accept
屬於請求頭,Content-Type
屬於實體頭。 Http報頭分爲通用報頭,請求報頭,響應報頭和實體報頭。- 請求方的http報頭結構:通用報頭|請求報頭|實體報頭
- 響應方的http報頭結構:通用報頭|響應報頭|實體報頭
-
Accept代表發送端(客戶端)希望接受的數據類型。 比如:
Accept:application/json
代表客戶端希望接受的數據類型是json類型,後臺返回json數據Content-Type
代表發送端(客戶端|服務器)發送的實體數據的數據類型。- 比如:
Content-Type:application/json
;代表發送端發送的數據格式是json, 後臺就要以這種格式來接收前端發過來的數據。 - 二者合起來,
Accept:application/json
;Content-Type:application/json
;即代表希望接受的數據類型是json格式,本次請求發送的數據的數據格式也是json格式。
-
四、RESTful服務開發
4.1 Java中常見的RESTful開發框架
4.1.1 jersey
-
Jersey RESTful 框架是開源的RESTful框架, 實現了JAX-RS (JSR 311 & JSR 339) 規範。它擴展了JAX-RS 參考實現, 提供了更多的特性和工具, 可以進一步地簡化 RESTful service 和 client 開發。儘管相對年輕,它已經是一個產品級的 RESTful service 和 client 框架。
-
優點:
- 優秀的文檔和例子
- 快速
- 平滑的 JUnit 集成
- 就個人而言, 當開發 RESTful service 時, JAX-RS(使用RESTful 風格來開發web service服務的規範) 實現要好於 MVC 框架。
- 可以集成到其它庫/框架 (Grizzly, Netty). 這也可能是很多產品使用它的原因。
- 支持異步鏈接
- 不喜歡 servlet container? 使用Jersey的時候可以不用它們。
- WADL, XML/JSON support包含在Glassfish中
-
缺點:
- Jersey 2.0+使用了有些複雜的依賴注入實現 一大堆第三方庫只支持 Jersey 1.X, 在 Jersey 2.X 不可用
-
4.1.2 play
-
Play Framework:使用Play Framework 很容易地創建,構建和發佈 web 應用程序,支持 Java & Scala。它使用Akka, 基於一個輕量級的無狀態的架構。它應該應用於大規模地低CPU和內存消耗的應用。
-
優點
- 易於開發
- 快,但是沒有其它的一些框架快
- 基於 Netty, 支持非阻塞的 I/O. 並行處理遠程調用的時候很優秀
- 社區很大
- 快速的項目構建和啓動
- 模塊化
- MVC
- REST, JSON/XML, Web Sockets, non-blocking I/O
- 只需刷新瀏覽器就可以看到最新的改變
- 支持Async
- 有出版的書
-
缺點
- 版本2.0 是最有爭議的Java框架。 切換至Scala會比較頭痛.
- 不向後兼容; Play 2.X 重寫了
- 號稱輕量級,但有些臃腫
- SBT構建工具. 號稱 Maven 殺手, 但是從沒有優秀到替換它。
- 難以學習和配置非 servlet
-
4.1.3 SpringMVC
4.2 使用SpringMVC開發RESTful服務
4.2.1 API接口測試——Postman
4.2.2 註解詳解
@RequestMapping參數化
@RequestMapping(path = "/employees", method = RequestMethod.GET)
等價於@GetMapping("/employees")
/**
* 員工資源控制器
*/
@Controller
public class EmployeeController {
/**
* 獲取所有員工
* 1. 確定資源 /employees
* 2. 確定請求方式 GET
* 3. 確定返回結果(類型、頭信息、狀態碼), 員工集合, content-type=application/json, 200
* @return
*/
@RequestMapping(path = "/employees", method = RequestMethod.GET)
//@GetMapping("/employees")
@ResponseBody
public List<Employee> findAll() {
ArrayList<Employee> list = new ArrayList<>();
list.add(new Employee(1L, "張三"));
list.add(new Employee(2L, "李四"));
return list;
}
}
@RequestMapping
參數path
等價於value
,爲路徑method
爲請求方法RequestMethod.GET
RequestMethod.POST
…
params
:規定請求時必須指定的參數名和參數值headers
:規定請求時必須帶有指定的頭信息consumes
:消費,相當於配置了headers = "content-type/html"
,服務器要求客戶端請求必須要有的請求頭信息produces
:生產,相當於配置了headers="accept=text/html"
,客戶端要求服務器必須生產出固定格式的內容
package com.ruki.restful.controller;
import com.ruki.restful.domain.Employee;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("test")
public class TestController {
/**
* params是規定請求時必須指定的參數名和參數值
* 代碼例子:代表請求時,必須帶有name參數,並且值也必須是admin
*/
@RequestMapping(params = "name=admin")
public void test() {
System.out.println("test方法");
}
/**
* @RequestParam 註解,規定請求時必須帶有指定的參數,但是參數值不規定
*
* @param username
* @param password
*/
@RequestMapping("/login")
public void login(@RequestParam String username, @RequestParam String password) {
System.out.println("login方法");
}
/**
* headers規定,請求時必須帶有指定的頭信息
*/
@RequestMapping(value = "/header", headers = "content-type=text/html")
public void header1() {
System.out.println("headers1...html");
}
@RequestMapping(value = "/header", headers = "content-type=text/xml")
public void header2() {
System.out.println("header2...xml");
}
/**
* 消費
* 相當於配置了 headers = "content-type/xml",
* 前臺傳過來xml數據,後臺纔會消費(吃)這個數據,並運行之後的程序
*/
@RequestMapping(value = "/consumes", consumes = "text/xml")
public void consumes1() {
System.out.println("consume方法...xml");
}
@RequestMapping(value = "/consumes", consumes = "text/html")
public void consumes2() {
System.out.println("consume方法...html");
}
/**
* 生產
* 前臺希望後臺生產出html頁面
* 相當於配置了headers="accept=text/html"
* 還相當於響應的頭信息中有 content-type=text/xml
*/
@RequestMapping(value = "/produces", produces = "text/html")
public void produce() {
System.out.println("produce...html");
}
/**
* @RequestBody 把請求體中的所有內容都封裝到指定的對象中
* @param employee
*/
@RequestMapping("/employee")
public void test1(@RequestBody Employee employee) {
// (@RequestBody String str) , 先String的json字符串,然後自己再用JSON工具轉換也可以
System.out.println(employee);
}
}
@PathVariable
- 必須使用該註解獲取路徑上的參數
package com.ruki.restful.controller;
import com.ruki.restful.domain.Employee;
import com.ruki.restful.domain.Salary;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* 員工資源控制器
*/
@Controller
public class EmployeeController {
/**
* 獲取某個員工的信息(路徑傳參)
* 1. 確定資源 /employees/{name} 使用路徑佔位符{}
* 2. 確定請求方式 GET
* 3. 確定返回結果(類型、頭信息、狀態碼), 員工對象, content-type=application/json, 201
*
* 如果@PathVariable 註解沒有設置value,默認就是去路徑上找相同名稱的參數
* @return
*/
@GetMapping("/employees/{id}")
@ResponseBody
public Employee getById(@PathVariable("id") Long id) {
// 由於從路徑中取參數,所以必須在參數前加上 @PathVariable
// 如果有很多參數最好加上指定的名字
// 不加value , 默認找名字相同的
// 不然只能取 ?id=2 這種形式的參數
return new Employee(id, "tempEmp");
}
/**
* 刪除某個員工(路徑傳參)
* 1. 確定資源 /employees/{name} 使用路徑佔位符{}
* 2. 確定請求方式 DELETE
* 3. 確定返回結果(類型、頭信息、狀態碼), 員工對象, 空文檔, 204
*
* 如果@PathVariable 註解沒有設置value,默認就是去路徑上找相同名稱的參數
* @return
*/
@DeleteMapping("/employees/{id}")
@ResponseBody
public void deleteEmp(Long id, HttpServletResponse response) {
System.out.println("成功刪除id爲" + id + "的員工!");
response.setStatus(HttpServletResponse.SC_NO_CONTENT);//204 狀態碼,沒有內容的返回值
}
/**
* 獲取某個員工某個月的薪資記錄(路徑傳參)
* 1. 確定資源 /employees/{id}/salaries/{month}
* 2. 確定請求方式 GET
* 3. 確定返回結果(類型、頭信息、狀態碼), 薪資對象, content-type=application/json, 200
*
* @DateTimeFormat 前臺傳日期參數到後臺接收時使用的註解,就是隻負責把這個格式的日期找到,領進來後就不管了
* @JsonFormat 後臺返回json數據給前臺時使用的註解
*
* @return
*/
@GetMapping("/employees/{id}/salaries/{month}")
@ResponseBody
public Salary getSalaryById(@PathVariable("id") Long empId,
@PathVariable("month") @DateTimeFormat(pattern = "yyyy-MM") Date month) {
return new Salary(1L, empId, BigDecimal.valueOf(100), month);
}
/**
* 給某個員工添加薪資記錄(路徑傳參)
* 1. 確定資源 /employees/{id}/salaries 其餘month、money參數以表單形式傳入 postman -> body -> x-www-form-urlencoded
* 2. 確定請求方式 POST
* 3. 確定返回結果(類型、頭信息、狀態碼), 薪資對象,content-type=application/json, 201
*
* 在Salary實體類的date上,加上@DateTimeFormat @JsonFormat 這兩個註解
* 保證按固定格式保存到數據庫,按固定格式返回出去
*
* 路徑佔位符中的參數,可以自動封裝到自定義對象中的同名屬性上
* @return
*/
@PostMapping("/employees/{employeeId}/salaries")
@ResponseBody
public Salary save(Salary salary) { //路徑參數名 和 表單參數名 必須和對象實體類的名字一樣 纔會自動封裝綁定
return salary;
}
@PutMapping("/employees/{name}")
@ResponseBody
public Employee update(Employee emp) { //路徑參數名 和 表單參數名 必須和對象實體類的名字一樣 纔會自動封裝綁定
System.out.println("PUT方法:" + emp);
return emp;
}
}
@RestController
@RestController
=@ResponseBody
+@Controller
- 前者將返回值轉化爲JSON類型
- 後者爲控制器的標識
@RequestBody
- 將前臺傳入的JSON對象,綁定實體類對象
4.3 請求處理
4.3.1 ajax
- 需要在
web.xml
文件中配置過濾器httpPutFormContentFilter
,Controller
方法才能將put
請求方式的參數成功接收。
//ajax請求
<script src="js/jquery-2.1.0.min.js"></script>
<script>
$(function() {
$("#btn").click(function () {
$.ajax({
url: "/employees/{name}",
type: "put", //PUT請求方式
data: {
name: "張三" //要傳的ajax參數,必須配置過濾器,不然無法接收到參數
}
});
});
});
</script>
httpPutFormContentFilter
過濾器
<!-- 處理put或patch請求方式的過濾器-->
<filter>
<filter-name>httpPutFormContentFilter</filter-name>
<filter-class>org.springframework.web.filter.HttpPutFormContentFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>httpPutFormContentFilter</filter-name>
<servlet-name>dispatcherServlet</servlet-name>
</filter-mapping>
4.3.2 form
- form表單中只有
GET
和POST
兩種請求方法,必須設置HiddenHttpMethodFilter
過濾器,以隱藏域的形式,將PUT
請求方式傳入到後臺 <input type="hidden" name="_method" value="put">
name
必須設置爲_method
- 源碼判斷的名稱是
_method
- 源碼判斷的名稱是
<form action="/employees/{name}" method="post">
<input type="hidden" name="_method" value="put">
<input type="text" name="name">
<input type="submit" value="提交">
</form>
HiddenHttpMethodFilter
過濾器
<!-- 瀏覽器不支持put,delete等method,由該filter將/blog?_method=delete轉換爲標準的http delete方法 -->
<filter>
<filter-name>HiddenHttpMethodFilter</filter-name>
<filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>HiddenHttpMethodFilter</filter-name>
<servlet-name>dispatcherServlet</servlet-name>
</filter-mapping>