Spring HATEOAS 是開發 REST 應用程式中常出現的概念,這個名詞在我工作中並沒有出現過,但往往自學就是要接觸自己平時不熟悉的事物。在哭完 “我已經學不動啦!”之後,就讓我們一起來看看 Spring HATEOAS吧。
HATEOAS是種規範,與我們常用的JSON概念相近, HATEOAS 的目的是,除了已簡潔的JSON格式回應客戶端請求外,另外告知客戶端如何取得下一步的資源,與 JSON 相似但不完全一致。為了好好的說明它,我們先介紹兩種http header 的 Content-Type
- application/json
- application/hal+json
第一種是傳統的 JSON格式,另一種則是傳統的 JSON加上 HAL 的支援。 HAL 就是實作 HATEOAS 的方法,不多說,直接SHOW YOU THE CODE。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| { "transactionNumber": "Fool001", "transactionResult": "SUCCESS" }
{ "transactionConfigId": "013", "transactionNumber": "Fool001", "transactionId": "12673", "transactionResult": "SUCCESS", "_links": { "self": { "href": "http://localhost:8080/transaction/12673" }, "transactionConfig": { "href": "http://localhost:8080/transactionConfig/013" } } }
|
已上方的例子來說, JSON只會回應交易單號與交易結果,但使用 HATEOAS 不僅可以得到結果,還可以得知下一步想要取得資源的方式,這樣的做法有不少好處。
- 幫助客戶端動態發現資源,若是
JSON客戶端必須要透過其他方式(比如說文件),才能了解更多回應的資訊。
- 除了連結以外,也可以包含一些操作,比方說更新、刪除等等。
- 寫在response裡面,減少了API更新必須同步外部的困擾。
如果是 Spring ,會使用 Spring HATEOAS 這個套件來實現,想要使用首先須引入套件
1 2 3 4
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-hateoas</artifactId> </dependency>
|
引入之後就可以開始使用,以下是一個簡單的範例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
| @RestController @RequestMapping("/payment") public class TransactionController {
@GetMapping("query/{transactionId}") public Transaction getTransaction(@PathVariable String transactionId) { Transaction transaction = new Transaction(); transaction.setTransactionConfigId("013"); transaction.setTransactionNumber("Fool001"); transaction.setTransactionId(transactionId); transaction.setTransactionResult("SUCCESS");
transaction.add(linkTo(methodOn(TransactionController.class).getTransaction(transactionId)) .withSelfRel());
transaction.add(Link.of("http://localhost:8080/payment/pay/" + transactionId) .withRel("pay"));
transaction.add(Link.of("http://localhost:8080/payment/refund/" + transactionId) .withRel("refund"));
transaction.add(Link.of("http://localhost:8080/payment/reverse/" + transactionId) .withRel("reverse"));
transaction.add(Link.of("http://localhost:8080/payment/settle/" + transactionId) .withRel("settle"));
return transaction; } }
public class Transaction extends RepresentationModel<Transaction> { private String transactionConfigId; private String transactionNumber; private String transactionId; private String transactionResult;
public String getTransactionConfigId() { return transactionConfigId; } public void setTransactionConfigId(String transactionConfigId) { this.transactionConfigId = transactionConfigId; } public String getTransactionNumber() { return transactionNumber; } public void setTransactionNumber(String transactionNumber) { this.transactionNumber = transactionNumber; } public String getTransactionId() { return transactionId; } public void setTransactionId(String transactionId) { this.transactionId = transactionId; } public String getTransactionResult() { return transactionResult; } public void setTransactionResult(String transactionResult) { this.transactionResult = transactionResult; } }
|
Transaction是一個物件,我建立了一個查詢交易的API,當客戶端查詢或是工程師們使用Postman進行測試時,會收到以下回應:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| GET http:
{ "transactionConfigId": "013", "transactionNumber": "Fool001", "transactionId": ${你輸入的訂單編號}, "transactionResult": "SUCCESS", "_links": { "self": { "href": "http://localhost:8080/payment/query/${你輸入的訂單編號}" }, "pay": { "href": "http://localhost:8080/payment/pay/${你輸入的訂單編號}" }, "refund": { "href": "http://localhost:8080/payment/refund/${你輸入的訂單編號}" }, "reverse": { "href": "http://localhost:8080/payment/reverse/${你輸入的訂單編號}" }, "settle": { "href": "http://localhost:8080/payment/settle/${你輸入的訂單編號}" } } }
|
用戶使用訂單查詢後可以順便帶入他的下一步可進行的動作,這麼做有以下好處:
- 靈活度 客戶端的API不必寫死,也可以不必要事先知道有哪些API路徑。
- 當API有標註版本時,還可以進行靈活的版本控制,比方說原本是1.0版推出了1.1版。
- 更好的描述性,客戶端可以很明確地知道自己的下一部可以怎麼做。