搭建 Restful Web 服務(wù)
1. 理解 REST
??REST 全稱是 Representational State Transfer,中文意思是表征性狀態(tài)轉(zhuǎn)移。它首次出現(xiàn)在2000年Roy Fielding的博士論文中,Roy Fielding是HTTP規(guī)范的主要編寫者之一。值得注意的是REST并沒有一個明確的標(biāo)準(zhǔn),而更像是一種設(shè)計的風(fēng)格。如果一個架構(gòu)符合REST的約束條件和原則,我們就稱它為RESTful架構(gòu)。
??理論上REST架構(gòu)風(fēng)格并不是綁定在HTTP上,只不過目前HTTP是唯一與REST相關(guān)的實例。
1.1. REST 原則
資源 可通過目錄結(jié)構(gòu)樣式的 URIs 暴露
表述 可以通過 JSON 或 XML 表達的數(shù)據(jù)對象或?qū)傩詠韨鬟f
消息 使用統(tǒng)一的 HTTP 方法(例如:GET、POST、PUT 和 DELETE)
無狀態(tài) 客戶端與服務(wù)端之間的交互在請求之間是無狀態(tài)的,從客戶端到服務(wù)端的每個請求都必須包含理解請求所必需的信息
1.2. HTTP 方法
??使用 HTTP 將 CRUD(create, retrieve, update, delete <創(chuàng)建、獲取、更新、刪除—增刪改查>)操作映射為 HTTP 請求。如果按照HTTP方法的語義來暴露資源,那么接口將會擁有安全性和冪等性的特性,例如GET和HEAD請求都是安全的, 無論請求多少次,都不會改變服務(wù)器狀態(tài)。而GET、HEAD、PUT和DELETE請求都是冪等的,無論對資源操作多少次, 結(jié)果總是一樣的,后面的請求并不會產(chǎn)生比第一次更多的影響。
1.2.1. GET
安全且冪等
獲取信息
1.2.2. POST
不安全且不冪等
使用請求中提供的實體執(zhí)行操作,可用于創(chuàng)建資源或更新資源
1.2.3. PUT
不安全但冪等
使用請求中提供的實體執(zhí)行操作,可用于創(chuàng)建資源或更新資源
1.2.4. DELETE
不安全但冪等
刪除資源
??POST和PUT在創(chuàng)建資源的區(qū)別在于,所創(chuàng)建的資源的名稱(URI)是否由客戶端決定。 例如為我的博文增加一個java的分類,生成的路徑就是分類名/categories/java,那么就可以采用PUT方法。不過很多人直接把POST、GET、PUT、DELETE直接對應(yīng)上CRUD,例如在一個典型的rails實現(xiàn)的RESTful應(yīng)用中就是這么做的。
1.3. HTTP status codes
??狀態(tài)碼指示 HTTP 請求的結(jié)果:
1XX:信息
2XX:成功
3XX:轉(zhuǎn)發(fā)
4XX:客戶端錯誤
5XX:服務(wù)端錯誤
1.4. 媒體類型
??HTTP頭中的 Accept 和 Content-Type 可用于描述HTTP請求中發(fā)送或請求的內(nèi)容。如果客戶端請求JSON響應(yīng),那么可以將 Accept 設(shè)為 application/json。相應(yīng)地,如果發(fā)送的內(nèi)容是XML,那么可以設(shè)置 Content-Type 為 application/xml 。
2. REST API 設(shè)計最佳實踐
??這里介紹一些設(shè)計 REST API 的最佳實踐,大家先記住下面這句話:
URL 是個句子,其中資源是名詞、HTTP 方法是動詞。
2.1. 使用名詞來表示資源
??下面是一些例子:
GET - /users:返回用戶列表
GET - /users/100:返回一個特定用戶
POST - /users:創(chuàng)建一個新用戶
PUT - /users/200:更新一個特定用戶
DELETE - /users/711:刪除一個特定用戶
??不要使用動詞:/getAllsers
/getUserById
/createNewUser
/updateUser
/deleteUser
2.2 在 HTTP 頭中使用適當(dāng)?shù)男蛄谢袷?/h1>
??客戶端和服務(wù)端都需要知道通信所用的格式,這個格式要在 HTTP 頭中指定:
Content-Type 定義請求格式
Accept 定義一個可接受的響應(yīng)格式列表
2.3 Get 方法和查詢參數(shù)不應(yīng)當(dāng)改變狀態(tài)
??使用 PUT, POST 和 DELETE 方法來改變狀態(tài),不要使用 GET 方法來改變狀態(tài):
GET /users/711?activate 或
GET /users/711/activate
2.4. 使用子資源表示關(guān)聯(lián)
??如果一個資源與另一個資源關(guān)聯(lián),使用子資源:
GET /cars/711/drivers/ 返回711號汽車的駕駛員列表
GET /cars/711/drivers/4 返回711號汽車的第4號駕駛員
2.5. 使用適當(dāng)?shù)?HTTP 方法 (動詞)
??再回顧一下這句話:
URL 是個句子,其中資源是名詞、HTTP 方法是動詞。
GET:獲取在URI資源中指定的表述,響應(yīng)消息體包含所請求資源的細節(jié)。
POST:創(chuàng)建一個URI指定的新資源,請求消息體提供新資源的細節(jié)。注意,POST也可以觸發(fā)一些操作,而不一定是要創(chuàng)建新資源。
PUT:創(chuàng)建或替代指定URI的資源。請求消息體指定要創(chuàng)建或更新的資源。
DELETE:移除指定URI的資源。
2.6. HTTP 響應(yīng)狀態(tài)碼
??當(dāng)客戶端通過API向服務(wù)端發(fā)起一個請求時,客戶端應(yīng)當(dāng)知道反饋:是否失敗、通過或者請求錯誤。HTTP 狀態(tài)碼是一批標(biāo)準(zhǔn)化代碼,在不同的場景下有不同的解釋。服務(wù)器應(yīng)當(dāng)總是返回正確的狀態(tài)碼。
??下面是重要的HTTP代碼分類:
2xx (成功分類):這些狀態(tài)碼代碼請求動作被接收且被服務(wù)器成功處理。
200:Ok 表示 GET、PUT 或 POST 請求的標(biāo)準(zhǔn)狀態(tài)碼。
201:Created(已創(chuàng)建)表示實例已被創(chuàng)建,多用于 POST 方法。
204:No Content(無內(nèi)容)表示請求已被成功處理但沒有返回任何內(nèi)容。常用于 DELETE 方法返回。
3xx (轉(zhuǎn)發(fā)分類)
304:Not Modified(無修改)表示客戶端已經(jīng)緩存此響應(yīng),無須再次傳輸相同內(nèi)容。
4xx (客戶端錯誤分類):這些狀態(tài)碼代表客戶端提交了一個錯誤請求。
400:Bad Request(錯誤請求)表示客戶端請求沒被處理,因為服務(wù)端無法理解客戶端請求。
401:Unauthorized(無授權(quán))表示客戶端無權(quán)訪問資源,應(yīng)當(dāng)加上所需的認證信息后再次請求。
403:Forbidden(禁止訪問)表示請求有效且客戶端已獲授權(quán),但客戶端無權(quán)訪問該資源。
404:Not Found(沒發(fā)現(xiàn))表示所請求的資源現(xiàn)在不可用。
410:Gone(移除)表示所請求的資源已被移除。
5xx (服務(wù)端錯誤分類)
500:Internal Server Error(內(nèi)部服務(wù)器錯誤)表示請求有效,但是服務(wù)端發(fā)生了異常。
503:Service Unavailable(服務(wù)不可用)表示服務(wù)器關(guān)閉或不可用,通常是指服務(wù)器處于維護狀態(tài)。
2.7. 名稱規(guī)約
??你可以遵循任何名稱規(guī)約,只要保持跨應(yīng)用一致性即可。如果請求體和響應(yīng)體是 JSON 類型,那么請遵循駝峰名稱規(guī)約。
2.8. 搜索、排序、過濾與分頁
??上面一些示例都是在一個數(shù)據(jù)集上的簡單查詢,對于復(fù)雜的數(shù)據(jù),我們需要在 GET 方法 API 上加一些參數(shù)來處理。下面是一些示例:
排序:這個例子中,客戶想獲取排序的公司列表,GET /companies 應(yīng)當(dāng)在查詢時接受多種排序參數(shù)。譬如 GET /companies?sort=rank_asc 將以等級升序的方式對公司進行排序。
過濾:要過濾數(shù)據(jù)集,我們可以通過查詢參數(shù)傳遞不同的選項。比如 GET /companies?category=banking&location=india 將過濾分類為銀行且位于印度的公司。
搜索:在公司列表中搜索公司名的 API 端點應(yīng)當(dāng)是 GET /companies?search=Digital。
分頁:當(dāng)數(shù)據(jù)集太大時,我們應(yīng)當(dāng)將數(shù)據(jù)集分割成小的數(shù)據(jù)塊,這樣有利于提升服務(wù)端性能,也方便客戶端處理響應(yīng)。如 GET /companies?page=23 意味著獲取公司列表的第 23 頁數(shù)據(jù)。
2.9. Restful API 版本
??一般使用不帶點的簡單數(shù)字表示版本,數(shù)字前加字母v代表版本號,如下所示:
/blog/api/v1
http://api.yourservice.com/v1/companies/34/employees
2.10. 處理 JSON 錯誤體
??API 錯誤處理機制是很重要的,而且要好好規(guī)劃。極力推薦總是在返回字段中包含錯誤消息。一個 JSON 錯誤體應(yīng)當(dāng)為開發(fā)者提供一些有用的信息:錯誤消息、錯誤代碼以及詳細描述。下面是一個較好的示例:
{ ? ?"code": 1234, ? ?"message": "Something bad happened :(", ? ?"description": "More details about the error here"}
2.11. 如何創(chuàng)建 Rest API URL
??推薦使用下面格式的 URL:
http(s)://{域名(:端口號)}/{表示REST API的值}/{API版本}/{識別資源的路徑}
http(s)://{表示REST API的域名(:端口號)}/{API 版本}/{識別資源的路徑}
??如下所示:http://example.com/api/v1/members/M000000001
http://api.example.com/v1/members/M000000001
3. 開發(fā)基于 Spring Boot 的 Restful Web 服務(wù)
??Spring Boot 提供了構(gòu)建企業(yè)應(yīng)用中 RESTful Web 服務(wù)的極佳支持。
3.1. 引入依賴
??要構(gòu)建 RESTful Web 服務(wù),我們需要在構(gòu)建配置文件中加上 Spring Boot Starter Web 依賴。
??對于 Maven 用戶,使用以下的代碼在 pom.xml 文件中加入依賴:
<dependency>
? <groupId>org.springframework.boot</groupId>
? <artifactId>spring-boot-starter-web</artifactId> ? ?</dependency>
??對于 Gradle 用戶,使用以下的代碼在 build.gradle 文件中加入依賴:
compile('org.springframework.boot:spring-boot-starter-web')
3.2. Rest 相關(guān)注解
??在繼續(xù)構(gòu)建 RESTful web 服務(wù)前,建議你先要熟悉下面的注解:
Rest Controller
??@RestController 注解用于定義 RESTful web 服務(wù)。它提供 JSON、XML 和自定義響應(yīng)。語法如下所示:
@RestControllerpublic class ProductServiceController {
}
Request Mapping
??@RequestMapping 注解用于定義請求 URI 以訪問 REST 端點。我們可以定義 Request 方法來消費 produce 對象。默認的請求方法是 GET:
@RequestMapping(value = "/products")public ResponseEntity<Object> getProducts() { }
Request Body ? @RequestBody 注解用于定義請求體內(nèi)容類型。public ResponseEntity<Object> createProduct(@RequestBody Product product) {
}
Path Variable
??@PathVariable 注解被用于定義自定義或動態(tài)的請求 URI,Path variable 被放在請求 URI 中的大括號內(nèi),如下所示:
public ResponseEntity<Object> updateProduct(@PathVariable("id") String id) {
}
Request Parameter
??@RequestParam 注解被用于從請求 URL 中讀取請求參數(shù)。缺省情況下是必須的,也可以為請求參數(shù)設(shè)置默認值。如下所示:
public ResponseEntity<Object> getProduct(
@RequestParam(value = "name", required = false, defaultValue = "honey") String name) {
}
3.3. 編寫 REST API
GET API
??下面的示例代碼定義了 HTTP GET 請求方法。在這個例子里,我們使用 HashMap 來在存儲 Product。注意我們使用了 POJO 類來存儲產(chǎn)品。
??在這里,請求 URI 是 /products,它會從 HashMap 倉儲中返回產(chǎn)品列表。下面的控制器類文件包含了 GET 方法的 REST 端點:
package com.tutorialspoint.demo.controller;import java.util.HashMap;import java.util.Map;import org.springframework.http.HttpStatus;import org.springframework.http.ResponseEntity;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import com.tutorialspoint.demo.model.Product;@RestControllerpublic class ProductServiceController { ? private static Map<String, Product> productRepo = new HashMap<>(); ? static {
? ? ?Product honey = new Product();
? ? ?honey.setId("1");
? ? ?honey.setName("Honey");
? ? ?productRepo.put(honey.getId(), honey);
? ? ?
? ? ?Product almond = new Product();
? ? ?almond.setId("2");
? ? ?almond.setName("Almond");
? ? ?productRepo.put(almond.getId(), almond);
? } ? @RequestMapping(value = "/products") ? public ResponseEntity<Object> getProduct() { ? ? ?return new ResponseEntity<>(productRepo.values(), HttpStatus.OK);
? }
}
POST API
??HTTP POST 請求用于創(chuàng)建資源。這個方法包含請求體。我們可以通過發(fā)送請求參數(shù)和路徑變量來定義自定義或動態(tài) URL。
??下面的示例代碼定義了 HTTP POST 請求方法。在這個例子中,我們使用 HashMap 來存儲 Product,這里產(chǎn)品是一個 POJO 類。
??這里,請求 URI 是 /products,在產(chǎn)品被存入 HashMap 倉儲后,它會返回字符串。
package com.tutorialspoint.demo.controller;import java.util.HashMap;import java.util.Map;import org.springframework.http.HttpStatus;import org.springframework.http.ResponseEntity;import org.springframework.web.bind.annotation.RequestBody;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestMethod;import org.springframework.web.bind.annotation.RestController;import com.tutorialspoint.demo.model.Product;@RestControllerpublic class ProductServiceController { ? private static Map<String, Product> productRepo = new HashMap<>(); ?
? @RequestMapping(value = "/products", method = RequestMethod.POST) ? public ResponseEntity<Object> createProduct(@RequestBody Product product) {
? ? ?productRepo.put(product.getId(), product); ? ? ?return new ResponseEntity<>("Product is created successfully", HttpStatus.CREATED);
? }
}
PUT API
??HTTP PUT 請求用于更新已有的資源。這個方法包含請求體。我們可以通過發(fā)送請求參數(shù)和路徑變量來定義自定義或動態(tài) URL。
??下面的例子展示了如何定義 HTTP PUT 請求方法。在這個例子中,我們使用 HashMap 更新現(xiàn)存的產(chǎn)品。此處,產(chǎn)品是一個 POJO 類。
??這里,請求 URI 是 /products/{id},在產(chǎn)品被存入 HashMap 倉儲后,它會返回字符串。注意我們使用路徑變量 {id} 定義需要更新的產(chǎn)品 ID:
package com.tutorialspoint.demo.controller;import java.util.HashMap;import java.util.Map;import org.springframework.http.HttpStatus;import org.springframework.http.ResponseEntity;import org.springframework.web.bind.annotation.PathVariable;import org.springframework.web.bind.annotation.RequestBody;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestMethod;import org.springframework.web.bind.annotation.RestController;import com.tutorialspoint.demo.model.Product;@RestControllerpublic class ProductServiceController { ? private static Map<String, Product> productRepo = new HashMap<>(); ?
? @RequestMapping(value = "/products/{id}", method = RequestMethod.PUT) ? public ResponseEntity<Object> updateProduct(@PathVariable("id") String id, @RequestBody Product product) {
? ? ?productRepo.remove(id);
? ? ?product.setId(id);
? ? ?productRepo.put(id, product); ? ? ?return new ResponseEntity<>("Product is updated successsfully", HttpStatus.OK);
? } ?
}
DELETE API
??HTTP Delete 請求用于刪除存在的資源。這個方法不包含任何請求體。我們可以通過發(fā)送請求參數(shù)和路徑變量來定義自定義或動態(tài) URL。
??下面的例子展示如何定義 HTTP DELETE 請求方法。這個例子中,我們使用 HashMap 來移除現(xiàn)存的產(chǎn)品,用 POJO 來表示。
??請求 URI 是 /products/{id} 在產(chǎn)品被從 HashMap 倉儲中刪除后,它會返回字符串。 我們使用路徑變量 {id} 來定義要被刪除的產(chǎn)品 ID。
package com.tutorialspoint.demo.controller;import java.util.HashMap;import java.util.Map;import org.springframework.http.HttpStatus;import org.springframework.http.ResponseEntity;import org.springframework.web.bind.annotation.PathVariable;import org.springframework.web.bind.annotation.RequestBody;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestMethod;import org.springframework.web.bind.annotation.RestController;import com.tutorialspoint.demo.model.Product;@RestControllerpublic class ProductServiceController { ? private static Map<String, Product> productRepo = new HashMap<>(); ?
? @RequestMapping(value = "/products/{id}", method = RequestMethod.DELETE) ? public ResponseEntity<Object> delete(@PathVariable("id") String id) {
? ? ?productRepo.remove(id); ? ? ?return new ResponseEntity<>("Product is deleted successsfully", HttpStatus.OK);
? }
}