使用 Spring HATEOAS 开发 REST 服务

Posted kaixinyufeng

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了使用 Spring HATEOAS 开发 REST 服务相关的知识,希望对你有一定的参考价值。

使用 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】所示

/** 代码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 类来说分别是 ListRestControllerListResource

          =》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 属性值如1所示。

如果在应用中使用自定义 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)); } }

代码【9】

Spring MVC 控制器 ListRestController 类通过“@RequestMapping”注解声明了其访问路径是“/lists”,

而访问单个资源的路径是类似“/lists/1”这样的形式。

在创建资源的链接时,指向单个资源的链接的 href 属性值是类似“http://localhost:8080/lists/1”这样的格式。

以上是关于使用 Spring HATEOAS 开发 REST 服务的主要内容,如果未能解决你的问题,请参考以下文章

Spring HATEOAS 与 Spring Data Rest

我可以让自定义控制器镜像 Spring-Data-Rest / Spring-Hateoas 生成的类的格式吗?

Angularjs + Spring Boot REST (HATEOAS) - 将角度变量传递给角度方法

在 Spring Boot 应用程序中使用 API 网关时,HATEOAS 路径无效

带有 RepositoryRestResource-s 和常规控制器的 Spring REST HATEOAS 中的根请求的自定义响应

强制spring hateoas生成https链接而不是http