HATEOAS(Hypermedia as the engine of application state)是 REST 架構風格中最複雜的約束,也是構建成熟 REST 服務的核心。它的重要性在於打破了客戶端和服務器之間嚴格的契約,使得客戶端能夠更加智能和自適應,而 REST 服務自己的演化和更新也變得更加容易。html
在介紹 HATEOAS 以前,先介紹一下 Richardson 提出的 REST 成熟度模型。該模型把 REST 服務按照成熟度劃分紅 4 個層次:java
從上述 REST 成熟度模型中能夠看到,使用 HATEOAS 的 REST 服務是成熟度最高的,也是推薦的作法。對於不使用 HATEOAS 的 REST 服務,客戶端和服務器的實現之間是緊密耦合的。客戶端須要根據服務器提供的相關文檔來了解所暴露的資源和對應的操做。當服務器發生了變化時,如修改了資源的 URI,客戶端也須要進行相應的修改。而使用 HATEOAS 的 REST 服務中,客戶端能夠經過服務器提供的資源的表達來智能地發現能夠執行的操做。當服務器發生了變化時,客戶端並不須要作出修改,由於資源的 URI 和其餘信息都是動態發現的。spring
本文將經過一個完整的示例來講明 HATEOAS。該示例是一個常見的待辦事項的服務,用戶能夠建立新的待辦事項、進行編輯或標記爲已完成。該示例中包含的資源以下:數組
應用提供相關的 REST 服務來完成對於列表和事項兩個資源的 CRUD 操做。服務器
若是 Web 應用基於 Spring 框架開發,那麼能夠直接使用 Spring 框架的子項目 HATEOAS 來開發知足 HATEOAS 約束的 Web 服務。本文的示例應用基於 Java 8 和使用 Spring Boot 1.1.9 來建立,Spring HATEOAS 的版本是 0.16.0.RELEASE。架構
知足 HATEOAS 約束的 REST 服務最大的特色在於服務器提供給客戶端的表達中包含了動態的連接信息,客戶端經過這些連接來發現能夠觸發狀態轉換的動做。Spring HATEOAS 的主要功能在於提供了簡單的機制來建立這些連接,並與 Spring MVC 框架有很好的集成。對於已有的 Spring MVC 應用,只須要一些簡單的改動就能夠知足 HATEOAS 約束。對於一個 Maven 項目來講,只須要添加代碼清單 1中的依賴便可。mvc
1
2
3
4
5
|
<
dependency
>
<
groupId
>org.springframework.hateoas</
groupId
>
<
artifactId
>spring-hateoas</
artifactId
>
<
version
>0.16.0.RELEASE</
version
>
</
dependency
>
|
REST 架構中的核心概念之一是資源。服務器提供的是資源的表達,一般使用 JSON 或 XML 格式。在通常的 Web 應用中,服務器端代碼會對所使用的資源建模,提供相應的模型層 Java 類,這些模型層 Java 類一般包含 JPA 相關的註解來完成持久化。在客戶端請求時,服務器端代碼經過 Jackson 或 JAXB 把模型對象轉換成 JSON 或 XML 格式。代碼清單 2給出了示例應用中表示列表的模型類 List 的聲明。app
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
|
@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 類的對象時,服務器端返回如代碼清單 3所示的 JSON 格式的表達。框架
1
2
3
4
|
{
"id": 1,
"name": "Default"
}
|
在代碼清單 3中,服務器端返回的只是模型類對象自己的內容,並無提供相關的連接信息。爲了把模型對象類轉換成知足 HATEOAS 要求的資源,須要添加連接信息。Spring HATEOAS 使用 org.springframework.hateoas.Link 類來表示連接。Link 類遵循 Atom 規範中對於連接的定義,包含 rel 和 href 兩個屬性。屬性 rel 表示的是連接所表示的關係(relationship),href 表示的是連接指向的資源標識符,通常是 URI。資源一般都包含一個屬性 rel 值爲 self 的連接,用來指向該資源自己。ide
在建立資源類時,能夠繼承自 Spring HATEOAS 提供的 org.springframework.hateoas.Resource 類,Resource 類提供了簡單的方式來建立連接。代碼清單 4給出了與模型類 List 對應的資源類 ListResource 的聲明。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
public class ListResource extends Resource {
private final 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;
}
}
|
如代碼清單 4所示,ListResource 類繼承自 Resource 類並對 List 類的對象進行了封裝,添加了兩個連接。在使用 ListResource 類以後,服務器端返回的表達格式如代碼清單 5所示。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
{
"list": {
"id": 1,
"name": "Default"
},
"links": [
{
"rel": "self",
"href": "http://localhost:8080/lists/1"
},
{
"rel": "items",
"href": "http://localhost:8080/lists/1/items"
}
]
}
|
代碼清單 5的 JSON 內容中添加了額外的 links 屬性,幷包含了兩個連接。不過模型類對象的內容被封裝在屬性 list 中。這是由於 ListResource 類直接封裝了整個的 List 類的對象,而不是把 List 類的屬性提取到 ListResource 類中。若是須要改變輸出的 JSON 表達的格式,可使用另一種封裝方式的 ListResource 類,如代碼清單 6所示。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
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;
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
{
"id": 1,
"name": "Default",
"links": [
{
"rel": "self",
"href": "http://localhost:8080/lists/1"
},
{
"rel": "items",
"href": "http://localhost:8080/lists/1/items"
}
]
}
|
這兩種不一樣的封裝方式各有優缺點。第一種方式的優勢是實現起來很簡單,只須要把模型層的對象直接包裝便可;第二種方式雖然實現起來相對比較複雜,可是能夠對資源的表達格式進行定製,使得資源的表達格式更直接。
在代碼實現中常常會須要把模型類對象轉換成對應的資源對象,如把 List 類的對象轉換成 ListResource 類的對象。通常的作法是經過「new ListResource(list)」這樣的方式來進行轉換。可使用 Spring HATEOAS 提供的資源組裝器把轉換的邏輯封裝起來。資源組裝器還能夠自動建立 rel 屬性爲 self 的連接。代碼清單 8中給出了組裝資源類 ListResource 的 ListResourceAssembler 類的實現。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
public class ListResourceAssembler extends ResourceAssemblerSupport<
List
, ListResource> {
public ListResourceAssembler() {
super(ListRestController.class, ListResource.class);
}
@Override
public ListResource toResource(List list) {
ListResource resource = createResourceWithId(list.getId(), list);
return resource;
}
@Override
protected ListResource instantiateResource(List entity) {
return new ListResource(entity);
}
}
|
在建立 ListResourceAssembler 類的對象時須要指定使用資源的 Spring MVC 控制器 Java 類和資源 Java 類。對於 ListResourceAssembler 類來講分別是 ListRestController 和 ListResource。ListRestController 類在下一節中會具體介紹,其做用是用來建立 rel 屬性爲 self 的連接。ListResourceAssembler 類的 instantiateResource 方法用來根據一個模型類 List 的對象建立出 ListResource 對象。ResourceAssemblerSupport 類的默認實現是經過反射來建立資源對象的。toResource 方法用來完成實際的轉換。此處使用了 ResourceAssemblerSupport 類的 createResourceWithId 方法來建立一個包含 self 連接的資源對象。
在代碼中須要建立 ListResource 的地方,均可以換成使用 ListResourceAssembler,如代碼清單 9所示。
1
2
3
4
5
|
//組裝單個資源對象
new ListResourceAssembler().toResource(list);
//組裝資源對象的集合
new ListResourceAssembler().toResources(lists);
|
代碼清單 9中的 toResources 方法是 ResourceAssemblerSupport 類提供的。當須要轉換一個集合的資源對象時,這個方法很是實用。
HATEOAS 的核心是連接。連接的存在使得客戶端能夠動態發現其所能執行的動做。在上一節中介紹過連接由 rel 和 href 兩個屬性組成。其中屬性 rel 代表了該連接所表明的關係含義。應用能夠根據須要爲連接選擇最適合的 rel 屬性值。因爲每一個應用的狀況並不相同,對於應用相關的 rel 屬性值並無統一的規範。不過對於不少常見的連接關係,IANA 定義了規範的 rel 屬性值。在開發中可能使用的常見 rel 屬性值如表1所示。
若是在應用中使用自定義 rel 屬性值,通常的作法是屬性值所有爲小寫,中間使用「-」分隔。
連接中另一個重要屬性 href 表示的是資源的標識符。對於 Web 應用來講,一般是一個 URL。URL 必須指向的是一個絕對的地址。在應用中建立連接時,在 URL 中使用硬編碼的主機名和端口號顯然不是好的選擇。Spring MVC 提供了相關的工具類能夠獲取 Web 應用啓動時的主機名和端口號,不過建立動態的連接 URL 還須要能夠獲取資源的訪問路徑。對於一個典型的 Spring MVC 控制器來講,其聲明如代碼清單 10所示。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
@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));
}
}
|
從代碼清單 10中能夠看到,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 控制器動態生成連接。代碼清單 11給出了建立單個資源的連接的方式。
1
2
3
|
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 類中包含的方法。如代碼清單 12所示。
1
|
Link link = linkTo(methodOn(ItemRestController.class).readItems(listId)).withRel("items");
|
代碼清單 12中的連接使用的是 ItemRestController 類中的 readItems 方法。參數 listId 是組成 URI 的一部分,在調用 readItems 方法時須要提供。
上面介紹的是經過 Spring MVC 控制器來建立連接,另一種作法是從模型類中建立。這是由於控制器一般用來暴露某個模型類。如 ListRestController 類直接暴露模型類 List,並提供了訪問 List 資源集合和單個 List 資源的接口。對於這樣的狀況,並不須要經過控制器來建立相關的連接,而可使用 EntityLinks。
首先須要在控制器類中經過「@ExposesResourceFor」註解聲明其所暴露的模型類,如代碼清單 13中的 ListRestController 類的聲明。
1
2
3
4
5
6
|
@RestController
@ExposesResourceFor(List.class)
@RequestMapping("/lists")
public class ListRestController {
}
|
另外在 Spring 應用的配置類中須要經過「@EnableEntityLinks」註解來啓用 EntityLinks 功能。此外還須要添加代碼清單 14中給出的 Maven 依賴。
1
2
3
4
5
|
<
dependency
>
<
groupId
>org.springframework.plugin</
groupId
>
<
artifactId
>spring-plugin-core</
artifactId
>
<
version
>1.1.0.RELEASE</
version
>
</
dependency
>
|
在須要建立連接的代碼中,只須要經過依賴注入的方式添加對 EntityLinks 的引用,就可使用 linkForSingleResource 方法來建立指向單個資源的連接,如代碼清單 15所示。
1
2
3
4
|
@Autowired
private EntityLinks entityLinks;
entityLinks.linkForSingleResource(List.class, 1)
|
須要注意的是,爲了 linkForSingleResource 方法能夠正常工做,控制器類中須要包含訪問單個資源的方法,並且其「@RequestMapping」是相似「/{id}」這樣的形式。
在添加了連接以後,服務器端提供的表達能夠幫助客戶端更好的發現服務器端所支持的動做。在具體的表達中,應用雖然能夠根據須要選擇最適合的格式,可是在表達的基本結構上應該遵循必定的規範,這樣能夠保證最大程度的適用性。這個基本結構主要是總體的組織方式和連接的格式。HAL(Hypertxt Application Language)是一個被普遍採用的超文本表達的規範。應用能夠考慮遵循該規範,Spring HATEOAS 提供了對 HAL 的支持。
HAL 規範自己是很簡單的,代碼清單 16給出了示例的 JSON 格式的表達。
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
|
{
"_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 規範圍繞資源和連接這兩個簡單的概念展開。資源的表達中包含連接、嵌套的資源和狀態。資源的狀態是該資源自己所包含的數據。連接則包含其指向的目標(URI)、所表示的關係和其餘可選的相關屬性。對應到 JSON 格式中,資源的連接包含在_links 屬性對應的哈希對象中。該_links 哈希對象中的鍵(key)是連接的關係,而值(value)則是另一個包含了 href 等其餘連接屬性的對象或對象數組。當前資源中所包含的嵌套資源由_embeded 屬性來表示,其值是一個包含了其餘資源的哈希對象。
連接的關係不只是區分不一樣連接的標識符,一樣也是指向相關文檔的 URL。文檔用來告訴客戶端如何對該連接所指向的資源進行操做。當開發人員獲取到了資源的表達以後,能夠經過查看連接指向的文檔來了解如何操做該資源。
使用 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 一種超媒體表達格式,只須要在應用的配置類上添加「@EnableHypermediaSupport(type= {HypermediaType.HAL})」註解就能夠啓用該超媒體支持。在啓用了超媒體支持以後,服務器端輸出的表達格式會遵循 HAL 規範。另外,啓用超媒體支持會默認啓用「@EnableEntityLinks」。在啓用超媒體支持以後,應用須要進行相關的定製使得生成的 HAL 表達更加友好。
首先是內嵌資源在_embedded 對應的哈希對象中的屬性值,該屬性值是由 org.springframework.hateoas.RelProvider 接口的實現來提供的。對於應用來講,只須要在內嵌資源對應的模型類中添加 org.springframework.hateoas.core.Relation 註解便可,如代碼清單 17所示。
1
2
3
|
@Relation(value = "list", collectionRelation = "lists")
public class List extends AbstractEntity {
}
|
代碼清單 17中聲明瞭當模型類 List 的對象做爲內嵌資源時,單個資源使用 list 做爲屬性值,多個資源使用 lists 做爲屬性值。
若是須要添加 Curie,則提供 org.springframework.hateoas.hal.CurieProvider 接口的實現,如代碼清單 18所示。利用已有的 org.springframework.hateoas.hal.DefaultCurieProvider 類並提供 Curie 的前綴和 URI 模板便可。
1
2
3
4
5
|
@Bean
public CurieProvider curieProvider() {
return new DefaultCurieProvider("todo",
new UriTemplate("http://www.midgetontoes.com/todolist/rels/{rel}"));
}
|