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}"));
}
|