使用 Spring HATEOAS 開發 REST 服務
學習博客:https://www.ibm.com/developerworks/cn/java/j-lo-SpringHATEOAS/
http://www.cnblogs.com/coderland/p/5902997.html
Spring HATEOAS 是一個用於支持實現超文本驅動的 REST Web 服務的開發庫。是 HATEOAS 的實現。
(HATEOAS背后的思想就是響應中包含指向其它資源的鏈接。客戶端可以利用這些鏈接和服務器交互)
非HATEOAS的響應例子:
GET /posts/1 HTTP/1.1 Connection: keep-alive Host: blog.example.com { "id" : 1, "body" : "My first blog post", "postdate" : "2015-05-30T21:41:12.650Z" }
HATEOAS的響應例子:
{ "id" : 1, "body" : "My first blog post", "postdate" : "2015-05-30T21:41:12.650Z", "links" : [ { "rel" : "self", "href" : http://blog.example.com/posts/1, "method" : "GET" } ] }
上面的例子中,每一個在links中的link都包含了三部分:
href:用戶可以用來檢索資源或者改變應用狀態的URI
rel:描述href指向的資源和現有資源的關系
method:和此URI需要的http方法
在rel中“self”表示了自描述的關系。如果一個資源包含其它資源,那么可以按照下面例子組織:
{ "id" : 1, "body" : "My first blog post", "postdate" : "2015-05-30T21:41:12.650Z", "self" : "http://blog.example.com/posts/1", "author" : "http://blog.example.com/profile/12345", "comments" : "http://blog.example.com/posts/1/comments", "tags" : "http://blog.example.com/posts/1/tags" }
上面的例子和前一個例子有些不同,沒有使用links數組
一、首先了解Rest架構
(一)REST 架構風格:
1、Rest架構風格已經成為了構建 Web 服務時應該遵循的事實標准。
2、很多 Web 服務和 API 都宣稱滿足了 REST 架構風格的要求,即所謂的“RESTful”服務。
不過就如同其他很多流行的概念一樣,不少人對於 REST 的含義還是存在或多或少的種種誤解。
REST 在某些時候被當成了一種營銷的手段。
不少所謂的“RESTful” Web 服務或 API 實際上並不滿足 REST 架構風格的要求。
這其中的部分原因在於 REST 的含義比較復雜,包含很多不同方面的內容。
(二)REST 是 Representational state transfer ---表達性狀態轉換
1、REST 是 Representational state transfer 的縮寫,翻譯為表達性狀態轉換。
2、REST 是一種架構風格,它包含了一個分布式超文本系統中對於組件、連接器和數據的約束。
3、REST 是作為互聯網自身架構的抽象而出現的,其關鍵在於所定義的架構上的各種約束。
只有滿足這些約束,才能稱之為符合 REST 架構風格。
【1】REST 的約束包括:
(1)客戶端-服務器結構
通過一個統一的接口來分開客戶端和服務器,使得兩者可以獨立開發和演化。
客戶端的實現可以簡化,而服務器可以更 容易的滿足可伸縮性的要求。
(2)無狀態
在不同的客戶端請求之間,服務器並不保存客戶端相關的上下文狀態信息。
任何客戶端發出的每個請求都包含了服務器處理該請求所需的全部信息。
(3)可緩存
客戶端可以緩存服務器返回的響應結果。服務器可以定義響應結果的緩存設置。
(4)分層的系統
在分層的系統中,可能有中間服務器來處理安全策略和緩存等相關問題,以提高系統的可伸縮性。
客戶端並不需要了解中間的這些層次的細節。
(5)按需代碼(可選)
服務器可以通過傳輸可執行代碼的方式來擴展或自定義客戶端的行為。這是一個可選的約束。
(6)統一接口
該約束是 REST 服務的基礎,是客戶端和服務器之間的橋梁。該約束又包含下面 4 個子約束。
A、資源標識符:
每個資源都有各自的標識符。客戶端在請求時需要指定該標識符。在 REST 服務中,該標識符通常是 URI。
客戶端所獲取的是資源的表達(representation),通常使用 XML 或 JSON 格式。
B、通過資源的表達來操縱資源
客戶端根據所得到的資源的表達中包含的信息來了解如何操縱資源,比如對資源進行修改或刪除。
C、自描述的消息
每條消息都包含足夠的信息來描述如何處理該消息。
D、超媒體作為應用狀態的引擎(HATEOAS)
客戶端通過服務器提供的超媒體內容中動態提供的動作來進行狀態轉換。
==》表達性狀態轉換
“表達性”是指對於資源的操縱都是通過服務器提供的資源的表達來進行的。
客戶端在根據資源的標識符獲取到資源的表達之后,從資源的表達中可以發現其可以使用的動作。
使用這些動作會發出新的請求,從而觸發狀態轉換。
二、HATEOAS 約束
HATEOAS(Hypermedia as the engine of application state)是 REST 架構風格中最復雜的約束,
也是構建成熟 REST 服務的核心。
它的重要性在於打破了客戶端和服務器之間嚴格的契約,使得客戶端可以更加智能和自適應,
而 REST 服務本身的演化和更新也變得更加容易。
(“hepermedia”表示任何包含指向圖片、電影、文字等資源的鏈接,Web是超媒體的經典例子)
(一)首先了解REST成熟度模型
REST 成熟度模型把 REST 服務按照成熟度划分成 4 個層次(成熟度由低-》高):
1、第一層次(Level 0)的 Web 服務只是使用 HTTP 作為傳輸方式,實際上只是遠程方法調用(RPC)的一種具體形式。
SOAP 和 XML-RPC 都屬於此類
2、第二層次(Level 1)的 Web 服務引入了資源的概念。每個資源有對應的標識符和表達
3、第三層次(Level 2)的 Web 服務使用不同的 HTTP 方法來進行不同的操作,並且使用 HTTP 狀態碼來表示不同的結果。
如:HTTP GET 方法來獲取資源,HTTP DELETE 方法來刪除資源
4、第四層次(Level 3)的 Web 服務使用 HATEOAS。在資源的表達中包含了鏈接信息。
客戶端可根據鏈接來發現可執行的動作
注意:
【1】不使用 HATEOAS 的 REST 服務:
客戶端和服務器的實現之間是緊密耦合的。客戶端需要根據服務器提供的相關文檔來了解所暴露的資源和對應的操作。當服務器發生了變化時,如修改了資源的 URI,客戶端也需要進行相應的修改。
【2】使用 HATEOAS 的 REST 服務:
客戶端可通過服務器提供的資源的表達來智能地發現可以執行的操作。當服務器發生了變化時,客戶端並不需要做出修改,因為資源的 URI 和其他信息都是動態發現的。
三、待辦事項示例--說明HATEOAS
【業務場景】:用戶可創建新待辦事項、進行編輯或標記為已完成。(張三的待辦事項列表中羅列具體待辦事項)
【示例資源】:1.用戶(應用中的用戶) 2.列表(待辦事項的列表,屬於某個用戶) 3.事項(具體的待辦事項,屬於某個列表)
【業務實現】:應用提供相關的 REST 服務來完成對於列表和事項兩個資源的 CRUD 操作
【應用技術】:Spring HATEOAS
如果 Web 應用基於 Spring 框架開發,那么可以直接使用 Spring 框架的子項目 HATEOAS 來開發滿足 HATEOAS 約束的 Web 服務。本文的示例應用基於 Java 8 和使用 Spring Boot 1.1.9 來創建,Spring HATEOAS 的版本是 0.16.0.RELEASE。
Step1:基本配置
【1】滿足 HATEOAS 約束的 REST 服務最大的特點:
服務器提供給客戶端的表達中包含了動態的鏈接信息,客戶端通過這些鏈接來發現可以觸發狀態轉換的動作
【2】為何應用Spring HATEOAS
Spring HATEOAS 的主要功能在於提供了簡單的機制來創建這些鏈接,並與 Spring MVC 框架有很好的集成。對於已有的 Spring MVC 應用,只需要一些簡單改動就可滿足 HATEOAS 約束。對於一個 Maven 項目來說,只需要添加如下依賴即可。
<!-- Spring HATEOAS 的 Maven 依賴聲明--> <dependency> <groupId>org.springframework.hateoas</groupId> <artifactId>spring-hateoas</artifactId> <version>0.16.0.RELEASE</version> </dependency>
Step2:資源
REST 架構中的核心概念之一是資源。服務器提供的是資源的表達,通常使用 JSON 或 XML 格式。在一般的 Web 應用中,服務器端代碼會對所使用的資源建模,提供相應的模型層 Java 類,這些模型層 Java 類通常包含 JPA 相關的注解來完成持久化。在客戶端請求時,服務器端代碼通過 Jackson 或 JAXB 把模型對象轉換成 JSON 或 XML 格式。如【代碼1】:
/** 代碼1:表示列表的模型類 List 的聲明。【1個用戶-》N個事項列表,1個事項列表-》N個具體事項】*/ @Entity public class List extends AbstractEntity { private String name; @ManyToOne @JsonIgnore private User user; @OneToMany(mappedBy = "list", fetch = FetchType.LAZY) @JsonIgnore private Set<Item> items = new HashSet<>(); protected List() { } public List(String name, User user) { this.name = name; this.user = user; } public String getName() { return name; } public User getUser() { return user; } public Set<Item> getItems() { return items; } }
當客戶端請求某個具體的 List 類的對象時,服務器端返回JSON 格式的表達。如【代碼2】
/** 代碼2:List 類的對象的 JSON 格式的表達*/ { "id": 1, "name": "Default" }
在【代碼2】中,服務器端返回的只是模型類對象本身的內容,並沒有提供相關的鏈接信息。為了把模型對象類轉換成滿足 HATEOAS 要求的資源,需要添加鏈接信息。
A、Spring HATEOAS 使用 org.springframework.hateoas.Link 類來表示鏈接。
B、Link 類遵循 Atom 規范中對於鏈接的定義,包含 rel 和 href 兩個屬性。
a、屬性 rel 表示的是鏈接所表示的關系(relationship)
b、href 表示的是鏈接指向的資源標識符,一般是 URI。
C、資源通常都包含一個屬性 rel 值為 self 的鏈接,用來指向該資源本身。
在創建資源類時,可以繼承自 Spring HATEOAS 提供的 org.springframework.hateoas.Resource 類,Resource 類提供了簡單的方式來創建鏈接。如【代碼3】
/** 代碼3:封裝方式一==》模型類 List 對應的資源類 ListResource 的聲明*/ public class ListResource extends Resource { private final List list; /** 對List對象進行封裝.實現簡單。只需要把模型層對象包裝即可*/ public ListResource(List list) { super(list); this.list = list; add(new Link("http://localhost:8080/lists/1")); add(new Link("http://localhost:8080/lists/1/items", "items")); } public List getList() { return list; } }
如【代碼3】所示,ListResource 類繼承自 Resource 類並對 List 類的對象進行了封裝,添加了兩個鏈接。在使用 ListResource 類之后,服務器端返回的表達格式如代碼【4】所示。
/** 代碼4:使用 ListResource 類之后的 JSON 格式的表達*/ { "list": { "id": 1, "name": "Default" }, "links": [ { "rel": "self", "href": "http://localhost:8080/lists/1" }, { "rel": "items", "href": "http://localhost:8080/lists/1/items" } ] }
代碼【4】的 JSON 內容中添加了額外的 links 屬性,並包含了兩個鏈接。不過模型類對象的內容被封裝在屬性 list 中。這是因為 ListResource 類直接封裝了整個的 List 類的對象,而不是把 List 類的屬性提取到 ListResource 類中。如果需要改變輸出的 JSON 表達的格式,可以使用另外一種封裝方式的 ListResource 類,如代碼【5】所示。
/** 代碼5:封裝方式二=》不同封裝格式的 ListResource 類,實現起來較方式一復雜,但是可以對資源的表達格式進行定制,使得資源的表達格式更直接*/ public class ListResource extends Resource { private final Long id; private final String name; public ListResource(List list) { super(list); this.id = list.getId(); this.name = list.getName(); add(new Link("http://localhost:8080/lists/1")); add(new Link("http://localhost:8080/lists/1/items", "items")); } public Long getId() { return id; } public String getName() { return name; } }
/** 代碼6:使用不同封裝方式的 JSON 格式的表達*/ { "id": 1, "name": "Default", "links": [ { "rel": "self", "href": "http://localhost:8080/lists/1" }, { "rel": "items", "href": "http://localhost:8080/lists/1/items" } ] }
對比封裝方式1[代碼3]&封裝方式2[代碼5]之間的差異:
兩種不同封裝方式各有優缺點:
方式1的優點實現起來很簡單,只需要把模型層的對象直接包裝即可;
方式2雖然實現起來相對比較復雜,但是可以對資源的表達格式進行定制,使得資源的表達格式更直接。
在代碼實現中經常會需要把模型類對象轉換成對應的資源對象,如把 List 類的對象轉換成 ListResource 類的對象。一般的做法是通過“new ListResource(list)”這樣的方式來進行轉換。可以使用 Spring HATEOAS 提供的資源組裝器把轉換的邏輯封裝起來。資源組裝器還可以自動創建 rel 屬性為 self 的鏈接。代碼【7】中給出了組裝資源類 ListResource 的 ListResourceAssembler 類的實現。
/** 代碼7:組裝資源類 ListResource 的 ListResourceAssembler 類的實現*/
/** ResourceAssemblerSupport類的默認實現是通過反射來創建資源對象的*/ public class ListResourceAssembler extends ResourceAssemblerSupport<List, ListResource> { public ListResourceAssembler() {
/** 指定使用資源的Spring MVC控制器java類和資源java類
ListRestController類作用:創建 rel 屬性為 self 的鏈接
*/ super(ListRestController.class, ListResource.class); }
/** toResource方法:用來完成實際的轉換*/ @Override public ListResource toResource(List list) {
/** 使用了 ResourceAssemblerSupport 類的 createResourceWithId 方法來創建一個包含 self 鏈接的資源對象*/ ListResource resource = createResourceWithId(list.getId(), list); return resource; }
/** instantiateResource方法:用來根據一個模型類 List 的對象創建出 ListResource 對象
*/ @Override protected ListResource instantiateResource(List entity) { return new ListResource(entity); } }
說明:
(1)在創建 ListResourceAssembler 類的對象時需要指定使用資源的 Spring MVC 控制器 Java 類和資源 Java 類。
對於 ListResourceAssembler 類來說分別是 ListRestController 和 ListResource。
=》ListRestController 類其作用是用來創建 rel 屬性為 self 的鏈接。
(2)ListResourceAssembler 類的 instantiateResource 方法用來根據一個模型類 List 的對象創建出 ListResource 對象。
(3)ResourceAssemblerSupport 類的默認實現是通過反射來創建資源對象的。
(4)toResource 方法用來完成實際的轉換。此處使用了 ResourceAssemblerSupport 類的 createResourceWithId 方法來創建一個包含 self 鏈接的資源對象。在代碼中需要創建 ListResource 的地方,都可以換成使用 ListResourceAssembler,如代碼【8】
/** 代碼[8]:使用 ListResourceAssembler 的示例*/ //組裝單個資源對象 toResources 方法是 ResourceAssemblerSupport 類提供的 new ListResourceAssembler().toResource(list); //組裝資源對象的集合 toResources 方法是 ResourceAssemblerSupport 類提供的 new ListResourceAssembler().toResources(lists);
三、鏈接
HATEOAS 的核心是鏈接。鏈接的存在使得客戶端可以動態發現其所能執行的動作。
鏈接由 rel 和 href 兩個屬性組成。
【1】屬性 rel 表明了該鏈接所代表的關系含義
應用可以根據需要為鏈接選擇最適合的 rel 屬性值。
由於每個應用的情況並不相同,對於應用相關的 rel 屬性值並沒有統一的規范。
不過對於很多常見的鏈接關系,IANA 定義了規范的 rel 屬性值。
如果在應用中使用自定義 rel 屬性值,一般的做法是屬性值全部為小寫,中間使用“-”分隔。
【2】屬性 href 表示的是資源的標識符
對於 Web 應用來說,通常是一個 URL。URL 必須指向的是一個絕對的地址。
在應用中創建鏈接時,在 URL 中使用硬編碼的主機名和端口號顯然不是好的選擇。
Spring MVC 提供了相關的工具類可以獲取 Web 應用啟動時的主機名和端口號,不過創建動態的鏈接 URL 還需要可以獲取資源的訪問路徑。
對於一個典型的 Spring MVC 控制器來說,其聲明如代碼【9】所示。
/**代碼[9]:Spring MVC 控制器 ListRestController 類的實現*/ @RestController @RequestMapping("/lists") public class ListRestController { @Autowired private ListService listService; @RequestMapping(method = RequestMethod.GET) public Resources<ListResource> readLists(Principal principal) { String username = principal.getName(); return new Resources<ListResource>(
new ListResourceAssembler().toResources(listService.findByUserUsername(username))); @RequestMapping(value = "/{listId}", method = RequestMethod.GET) public ListResource readList(@PathVariable Long listId) { return new ListResourceAssembler().toResource(listService.findOne(listId)); } }
Spring MVC 控制器 ListRestController 類通過“@RequestMapping”注解聲明了其訪問路徑是“/lists”,
而訪問單個資源的路徑是類似“/lists/1”這樣的形式。
在創建資源的鏈接時,指向單個資源的鏈接的 href 屬性值是類似“http://localhost:8080/lists/1”這樣的格式。
而其中的“/lists”不應該是硬編碼的,否則當修改了 ListRestController 類的“@RequestMapping”時,所有相關的生成鏈接的代碼都需要進行修改。
Spring HATEOAS 提供了 org.springframework.hateoas.mvc.ControllerLinkBuilder 來解決這個問題,
用來根據 Spring MVC 控制器動態生成鏈接。代碼【10】給出了創建單個資源的鏈接的方式。
/** 代碼[10]:使用 ControllerLinkBuilder 類創建鏈接*/ import static org.springframework.hateoas.mvc.ControllerLinkBuilder.*; Link link = linkTo(ListRestController.class).slash(listId).withSelfRel();
通過 ControllerLinkBuilder 類的 linkTo 方法,先指定 Spring MVC 控制器的 Java 類,再通過 slash 方法來找到下一級的路徑,最后生成屬性值為 self 的鏈接。在使用 ControllerLinkBuilder 生成鏈接時,除了可以使用控制器的 Java 類之外,還可以使用控制器 Java 類中包含的方法。如代碼【11】所示。
/** 代碼[11]通過控制器 Java 類中的方法生成鏈接*/ Link link = linkTo(methodOn(ItemRestController.class).readItems(listId)).withRel("items");
鏈接使用的是 ItemRestController 類中的 readItems 方法。
參數 listId 是組成 URI 的一部分,在調用 readItems 方法時需要提供。
上面介紹的是通過 Spring MVC 控制器來創建鏈接,另外一種做法是從模型類中創建。這是因為控制器通常用來暴露某個模型類。如 ListRestController 類直接暴露模型類 List,並提供了訪問 List 資源集合和單個 List 資源的接口。對於這樣的情況,並不需要通過控制器來創建相關的鏈接,而可以使用 EntityLinks。
首先需要在控制器類中通過“@ExposesResourceFor”注解聲明其所暴露的模型類,如代碼【12】中 ListRestController 類的聲明。
/** 代碼[12]“@ExposesResourceFor”注解的使用*/ @RestController @ExposesResourceFor(List.class) @RequestMapping("/lists") public class ListRestController { }
另:在 Spring 應用的配置類中需通過“@EnableEntityLinks”注解來啟用 EntityLinks 功能。需添加代碼【13】Maven 依賴。
/** 代碼[13]EntityLinks 功能所需的 Maven 依賴*/ <dependency> <groupId>org.springframework.plugin</groupId> <artifactId>spring-plugin-core</artifactId> <version>1.1.0.RELEASE</version> </dependency>
在需要創建鏈接的代碼中,只需要通過依賴注入的方式添加對 EntityLinks 的引用,就可以使用 linkForSingleResource 方法來創建指向單個資源的鏈接,如代碼【14】所示。
/**代碼14:使用 EntityLinks 創建鏈接 */ @Autowired private EntityLinks entityLinks; entityLinks.linkForSingleResource(List.class, 1)
需要注意的是,為了 linkForSingleResource 方法可以正常工作,控制器類中需要包含訪問單個資源的方法,而且其“@RequestMapping”是類似“/{id}”這樣的形式。
四、超媒體控制與 HAL
在添加了鏈接之后,服務器端提供的表達可以幫助客戶端更好的發現服務器端所支持的動作。
在具體的表達中,應用雖然可以根據需要選擇最適合的格式,但是在表達的基本結構上應該遵循一定的規范,
這樣可以保證最大程度的適用性。這個基本結構主要是整體的組織方式和鏈接的格式。
首先介紹JSON Hypermedia Types
JSON媒體類型沒有提供原生的超鏈接語法,所以為了解決這個問題,有幾種JSON超媒體類型被創建出來:
• HAL—http://stateless.co/hal_specification.html • JSON-LD—http://json-ld.org • Collection+JSON—http://amundsen.com/media-types/collection/ • JSON API—http://jsonapi.org/ • Siren—https://github.com/kevinswiber/siren
HAL是其中最流行的一種,而且被Spring Framework支持。
HAL(Hypertxt Application Language)是簡單的超媒體類型,一個被廣泛采用的超文本表達的規范。
HAL同時支持XML和JSON格式
應用可以考慮遵循該規范,Spring HATEOAS 提供了對 HAL 的支持。
HAL媒體類型定義了一種資源,它是狀態的容器、links的集合、嵌套資源的集合。如下圖所示
資源狀態是用JSON的key/value形式表達的。如下面所示:
{ "id" : 1, "body" : "My first blog post", "postdate" : "2015-05-30T21:41:12.650Z" }
HAL規范中定義,使用_links包含所有的link。如下面例子所示:
{ "id" : 1, "body" : "My first blog post", "postdate" : "2015-05-30T21:41:12.650Z", "_links" : { "self": { "href": "http://blog.example.com/posts/1" }, "comments": { "href": "http://blog.example.com/posts/1/comments", "totalcount" : 20 }, "tags": { "href": "http://blog.example.com/posts/1/tags" } } }
在HAL嵌套資源的情況,如下面例子所示:
{ "id" : 1, "body" : "My first blog post", "postdate" : "2015-05-30T21:41:12.650Z", "_links" : { "self": { "href": "http://blog.example.com/posts/1" }, "comments": { "href": "http://blog.example.com/posts/1/comments", "totalcount" : 20 }, "tags": { "href": "http://blog.example.com/posts/1/tags" } }, "_embedded" : { "author" : { "_links" : { "self": { "href": "http://blog.example.com/profile/12345" } }, "id" : 12345, "name" : "John Doe", "displayName" : "JDoe" } } }
HAL 規范:
HAL 規范本身是很簡單的,代碼【15】給出了示例的 JSON 格式的表達。
/** HAL 規范的示例 JSON 格式的表達*/ { "_links": { "self": { "href": "http://localhost:8080/lists" } }, "_embedded": { "lists": [ { "id": 1, "name": "Default", "_links": { "todo:items": { "href": "http://localhost:8080/lists/1/items" }, "self": { "href": "http://localhost:8080/lists/1" }, "curies": [ { "href": "http://www.midgetontoes.com/todolist/rels/{rel}", "name": "todo", "templated": true } ] } } ] } }
HAL 規范圍繞資源和鏈接這兩個簡單的概念展開。
(1)資源的表達中包含鏈接、嵌套的資源和狀態。資源的狀態是該資源本身所包含的數據。
(2)鏈接則包含其指向的目標(URI)、所表示的關系和其他可選的相關屬性。
【1】對應到 JSON 格式中
A。資源的鏈接包含在_links 屬性對應的哈希對象中。
該_links 哈希對象中的鍵(key)是鏈接的關系,
而值(value)則是另外一個包含了 href 等其他鏈接屬性的對象或對象數組。
B。當前資源中所包含的嵌套資源由_embeded 屬性來表示,其值是一個包含了其他資源的哈希對象。
鏈接的關系不僅是區分不同鏈接的標識符,同樣也是指向相關文檔的 URL。
文檔用來告訴客戶端如何對該鏈接所指向的資源進行操作。
當開發人員獲取到了資源的表達之后,可以通過查看鏈接指向的文檔來了解如何操作該資源。
【2】使用 URL 作為鏈接的關系帶來的問題:
URL 作為屬性名稱來說顯得過長,而且不同關系的 URL 的大部分內容是重復的。
為了解決這個問題,可以使用 Curie。Curie 可以作為鏈接關系 URL 的模板。
鏈接的關系聲明時使用 Curie 的名稱作為前綴,不用提供完整的 URL。
應用中聲明的 Curie 出現在_links 屬性中。
代碼中定義了 URI 模板為“http://www.midgetontoes.com/todolist/rels/{rel}”的名為 todo 的 Curie。
在使用了 Curie 之后,名為 items 的鏈接關系變成了包含前綴的“todo:items”的形式。
這就表示該鏈接的關系實際上是“http://www.midgetontoes.com/todolist/rels/items”。
Spring HATEOAS 的 HAL 支持
目前 Spring HATEOAS 僅支持 HAL 一種超媒體表達格式
只需在應用的配置類上添加“@EnableHypermediaSupport(type= {HypermediaType.HAL})”注解就可啟用該超媒體支持。
在啟用了超媒體支持之后,服務器端輸出的表達格式會遵循 HAL 規范。
另外,啟用超媒體支持會默認啟用“@EnableEntityLinks”。
在啟用超媒體支持之后,應用需要進行相關的定制使得生成的 HAL 表達更加友好。
首先是內嵌資源在_embedded 對應的哈希對象中的屬性值,
該屬性值是由 org.springframework.hateoas.RelProvider 接口的實現來提供的。
對於應用來說,只需要在內嵌資源對應的模型類中添加 org.springframework.hateoas.core.Relation 注解即可,
/** 代碼16:在模型類中添加 @Relation 注解*/ @Relation(value = "list", collectionRelation = "lists") public class List extends AbstractEntity { }
聲明了當模型類 List 的對象作為內嵌資源時,單個資源使用 list 作為屬性值,多個資源使用 lists 作為屬性值。
如果需要添加 Curie,則提供 org.springframework.hateoas.hal.CurieProvider 接口的實現,如代碼【17】。
利用已有的 org.springframework.hateoas.hal.DefaultCurieProvider 類並提供 Curie 的前綴和 URI 模板即可。
/** 代碼17:添加 CurieProvider 接口的實現*/ @Bean public CurieProvider curieProvider() { return new DefaultCurieProvider("todo", new UriTemplate("http://www.midgetontoes.com/todolist/rels/{rel}")); }
示例2:HATEOAS in Spring
<dependency> <groupId>org.springframework.hateoas</groupId> <artifactId>spring-hateoas</artifactId> <version>0.17.0.RELEASE</version> </dependency>
為了簡化超鏈接的嵌入,Spring HATEOAS提供了org. springframework.hateoas.ResourceSupport,一般應由資源類進行擴展。ResourceSupport類為增加/刪除鏈接提供了重載方法,它也包含了getId方法,此方法返回和資源相關的URI。getId的實現依據了REST的一個准則:一個資源的ID就是它的URI。下面的例子是在Spring中使用HATEOAS的代碼:
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; import static org.springframework.hateoas.mvc.ControllerLinkBuilder.methodOn; @RestController public class PollController { @RequestMapping(value="/polls", method=RequestMethod.GET) public ResponseEntity<Iterable<Poll>> getAllPolls() { Iterable<Poll> allPolls = pollRepository.findAll(); for(Poll p : allPolls) { updatePollResourceWithLinks(p); return new ResponseEntity<>(allPolls, HttpStatus.OK); } } @RequestMapping(value="/polls/{pollId}", method=RequestMethod.GET) public ResponseEntity<?> getPoll(@PathVariable Long pollId) { Poll p = pollRepository.findOne(pollId); updatePollResourceWithLinks(p); return new ResponseEntity<> (p, HttpStatus.OK); } private void updatePollResourceWithLinks(Poll poll) { poll.add(linkTo(methodOn(PollController.class).getAllPolls()).slash(poll.getPollId()).withSelfRel()); poll.add(linkTo(methodOn(VoteController.class).getAllVotes(poll.getPollId())).withRel("votes")); poll.add(linkTo(methodOn(ComputeResultController.class).computeResult(poll.getPollId())).withRel("compute-result")); } }
下圖是上面例子的響應:
總結:
在開發一個新的 Web 服務或 API 時,REST 架構風格已經成為事實上的標准。在開發時需要明白 REST 架構風格中所包含的約束的含義。HATEOAS 作為 REST 服務約束中最復雜的一個,目前還沒有得到廣泛的使用。但是采用 HATEOAS 所帶來的好處是很大的,可以幫助客戶端和服務器更好的解耦,可以減少很多潛在的問題。Spring HATEOAS 在 Spring MVC 框架的基礎上,允許開發人員通過簡單的配置來添加 HATEOAS 約束。如果應用本身已經使用了 Spring MVC,則同時啟用 HATEOAS 是一個很好的選擇。