在〈簡介 RestTemplate〉中,為了介紹怎麼使用 RestTemplate
,設置了一個簡單的 REST 網站,這些網站透過 HTTP 動詞,結合 URI,使用 JSON 作為交換媒介,JSON 的格式非常單純,例如請求 http://localhost:8080/messages/1
的話會傳回:
{
"text": "msg1"
}
如果請求 http://localhost:8080/messages
的話會傳回:
[
{
"text": "msg1"
},
{
"text": "msg2"
}
]
容易理解又單純的格式,除了資料本身的資訊之外,也沒別的了,也正因為沒別的資訊,你得在文件上記錄有哪些端點、互動的方式、API 之間的關係等,API 消費者若依文件來編寫了服務的請求等,哪天 API 變動,消費者也得跟著作出因應的修改,API 消費者與提供者之間有著緊密的關係。
還記得在〈@RepositoryRestResource〉中看到的 JSON 回應嗎?你怎麼知道有哪些 API 可以使用呢?因為 JSON 回應本身就包含了鏈結、參數等 API 相關的資訊,有了這些資訊,我們可以從這個 API 探尋到另一個 API,從而理解到怎麼使用網站上提供的服務。
而且,API 消費者可以編寫程式,自動導覽至相關 API,若 API 供應者變動了服務介面,由於 JSON 回應有著一致的格式,API 消費者可以自動因應變化。
那麼〈@RepositoryRestResource〉中的 JSON 回應有什麼規範嗎?若你察看回應標頭,會發現它的內容類型為 application/hal+json
,而不是 application/json
,當然,application/hal+json
還是一種 application/json
,只不過在 JSON 加上了 HAL - Hypertext Application Language 約束,也就是不再是隨意地 JSON 結構了。
Spring Data REST 實際上是透過 Spring HATEOAS,自動建立對應於 Repository 的相關 API…嗯?又多了一個名詞…HATEOAS?
這就得來聊聊 Leonard Richardson 在 QCon Talk 談到的 REST 成熟度模型了,這是個用來檢視、思考 REST 應用程度與方向的模型,它將 REST 的應用成熟度分為四個階段。
LEVEL 0 使用一個 URI 與一個 HTTP 方法,基本上就是單純使用 HTTP 作為傳輸協定,服務使用的 URI 只是個接收請求進行回應的端點,HTTP 方法只是用來發起請求,至於請求的相關細節,例如想進行的動作、必須提供的資料等,全部包含在發送過去的文件之中,像是 XML、JSON 等其他(自訂)格式,回應使用某個文件格式傳回,當中包含了請求操作後的結果。
你可以想像只使用一個 /messages
,它接受 POST
請求,想要查詢某個訊息、全部訊息、刪除訊息、修改訊息等,都在 POST
本體中使用某個格式的文件指定。
簡單來說,這個階段的應用就想像成是個連線程式,有指定的連線位置,傳送指定格式的封包,SOAP、XML-RPC 等是屬於這個階段的代表。
Level 1 使用多個 URI 與一個 HTTP 方法,URI 代表了資源,像是 /show_message
、/create_message
、/update_message
、/delete_message
都是資源,HTTP 方法只是用來發起請求,至於請求的細節由請求本體來提供,例如,在請求 /show_message
這項資源時,若包含 all
請求參數,表示顯示全部的訊息,若是 "id=1"
這類請求參數,表示顯示指定的訊息。
Level 2 使用多個 URI、多個 HTTP 方法,並善用 HTTP 回應狀態碼,URI 用來代表資源,像是 /messages
、/messages/1
,HTTP 方法用來表示想進行的操作,例如 GET /messages
表示取得全部訊息,GET /messages/1
表示取得指定訊息,POST /messages
表示新增訊息、DELETE /messages/1
表示刪除指定訊息等,〈簡介 RestTemplate〉的簡單應用程式就是此類。
Level 3 更進一步地,支援 HATEOAS(Hypermedia As The Engine Of Application State)的概念,就類似 HTML 頁面鏈結,你可以從這個頁面得知可通往哪些頁面,在 REST 的 Level 3 模型中,客戶端可以從某個資源,知道還有哪些其他相關的資源,以及如何對它進行操作,〈@RepositoryRestResource〉的範例專案,就是這一類。
HATEOAS 是個概念,實際上如何從一個資源得知其他的資源,該採用哪個格式,格式中該採用哪些識別名稱,需要有實作規範,HAL 就是實作規範之一,採用 JSON 格式、ATOM (RFC 4287) 鏈結語法等,也就是你在〈@RepositoryRestResource〉看到的格式。
Martin Fowler 在〈Richardson Maturity Model〉中,也有對 REST 成熟度模型作了詮譯,有興趣可以進一步閱讀。
如果你想要實作出可以支援 HATEOAS 概念的 REST 服務,可以使用 Spring HATEOAS,例如,要將〈簡介 RestTemplate〉中的專案,實作為支援 HATEOAS 概念,可以在 build.gradle 中加入:
implementation('org.springframework.boot:spring-boot-starter-hateoas')
想讓 Message
能轉換為 HAL 格式的方式之一,是繼承 ResourceSupport
,如此 Message
會有個可以加入 Link
實例的 add
方法可以使用,另一個方式是建構 Resource
時指定 Message
以及 Link
實例。
建立 Link 實例時,可以直接指定鏈結:
Link link = new Link("http://localhost:8080/messages/1");
因此,若 message
參考了 Message
實例,可以令控制器的處理方法傳回:
new Resource<Message>(message, new Link("http://localhost:8080/messages/1"));
不過,更有彈性的方式之一,可以在控制器標註 @RequestMapping
並指明根對應,然後透過 ControllerLinkBuilder
來建構 Link
,它有個 linkTo
方法,可以自省控制器類別找出資源的 URI 根對應,進一步地,還可以建構帶有 self
等資訊的 Link
實例,例如:
new Resource<Message>(
message,
linkTo(RestTmplApplication.class).slash("1").withSelfRel()
);
如果處理器傳回了以上實例,那麼最後的得到 JSON 格式會是:
{
"text": "msg1",
"_links": {
"self": {
"href": "http://localhost:8080/messages/1"
}
}
}
內容類型回應標頭也會自動變成 application/hal+json
,因此,可以將 RestTmplApplication
重構為:
package cc.openhome;
...略
@SpringBootApplication
@RestController
@RequestMapping("messages")
public class RestTmplApplication {
public static void main(String[] args) {
SpringApplication.run(RestTmplApplication.class, args);
}
List<Message> messages = new ArrayList<Message>() {{
add(new Message("msg1"));
add(new Message("msg2"));
}};
@GetMapping("/")
public Resources<Resource<Message>> index() {
List<Resource<Message>> reslt =
IntStream.range(0, messages.size())
.mapToObj(idx -> new Resource<>(messages.get(idx), link(String.valueOf(idx + 1))))
.collect(toList());
return new Resources<>(reslt, linkTo(RestTmplApplication.class).withSelfRel());
}
@GetMapping("/{id}")
public Resource<Message> show(@PathVariable("id") String id) {
return new Resource<>(messages.get(Integer.parseInt(id) - 1), link(id));
}
@PostMapping("/")
public Resource<Message> create(@RequestBody Message message) {
messages.add(message);
return new Resource<>(message);
}
@DeleteMapping("/{id}")
public Resource<Message> delete(@PathVariable("id") String id) {
return new Resource<>(messages.remove(Integer.parseInt(id) - 1));
}
private Link link(String id) {
return linkTo(RestTmplApplication.class).slash(id).withSelfRel();
}
}
在這邊留意到 Resources
,這可以用來包含多個 Message
與 Link
實例,傳回的 JSON 會像是:
{
"_embedded": {
"messageList": [
{
"text": "msg1",
"_links": {
"self": {
"href": "http://localhost:8080/messages/1"
}
}
},
{
"text": "msg2",
"_links": {
"self": {
"href": "http://localhost:8080/messages/2"
}
}
}
]
},
"_links": {
"self": {
"href": "http://localhost:8080/messages"
}
}
}
當然,傳回的 JSON 變得複雜多了,若想用 RestTemplate
來消費這個 JSON,會需要 HAL 轉換器,這就在下篇文件來談了。