聊一聊到底什么是 REST,顺便再来看下 Spring HATEOAS 的用法

苍烨然
2023-12-01

说起 REST API,小伙伴们多多少少都有听说过,但是如果让你详细介绍一下什么是 REST,估计会有很多人讲不出来,或者只讲出来其中一部分。

今天松哥就来和大家一起来聊一聊到底什么是 REST,顺便再来看下 Spring HATEOAS 的用法。

1. REST 成熟模型

首先关于 REST,有一个大佬 Leonard Richardson 为 REST 定义了一个成熟度模型,他一共定义了四个不同的层次,分别如下:

  1. Level0:Web 服务单纯的使用 HTTP 作为数据传输方式,本质上就是远程方法调用,常见的 SOAP 和 RPC 基本上都属于这一类。
  2. Level1:在这一级别上,引入了资源的概念,服务端的每一个资源,都有一个对应的操作地址。
  3. Level2:在这一级别上,我们引入了不同的 HTTP 请求方法来描述不同的操作,例如 GET 表示查询、POST 表示插入、PUT 表示更新、DELETE 表示删除,并且使用 HTTP 的状态码来表示不同的响应结果。一般来说,大家在日常的接口开发中,基本上都能做到这一层级。但是这还不是最佳结果。
  4. Level3:按照 Leonard Richardson 的意思,这一层级的 REST 基于 HATEOAS(Hypertext As The Engine Of Application State),在这一级别上,除了返回资源的 JSON 之外,还会额外返回一组 Link,这组 Link 描述了对于该资源可以做哪些操作,以及具体的该怎么做。

在日常的开发中,我们一般都是只实现到 Level2 这一层级,真正做到 Level3 的估计很少,不过虽然在工作中一般不会做到 Level3 这一层级,但是,我相信很多小伙伴应该是见过 Level3 层级的 REST 是啥样子的,特别是看过 vhr 视频的小伙伴,松哥在其中讲过,通过 Spring Data Jpa+Spring Rest Repositories 实现的 CURD 接口,其实就是一个达到了 Level3 层级的 REST。

2. Spring HATEOAS

那么接下来我先用 Spring HATEOAS 写一个简单的 REST,然后结合这个案例来和小伙伴们聊一聊到底 Spring HATEOAS 有何不一样的地方。

首先我们创建一个 Spring Boot 工程,引入 Web 和 Spring HATEOAS 依赖,如下:

创建好之后,我们首先创建一个 User 实体类:

public class User extends RepresentationModel {
    private Integer id;
    private String username;
    private String address;
    //省略 getter/setter
}

注意这个 User 实体类需要继承自 RepresentationModel,以方便后续添加不同的 Link(以前旧的版本需要继承自 ResourceSupport)。

接下来写一个简单的测试接口。

查询所有用户:

@RestController
@RequestMapping("/users")
public class UserController {

    @GetMapping
    public CollectionModel<user> list() {
        List<user> list = new ArrayList&lt;&gt;();
        User u1 = new User();
        u1.setId(1);
        u1.setUsername("javaboy");
        u1.setAddress("www.javaboy.org");
        u1.add(WebMvcLinkBuilder.linkTo(UserController.class).slash(u1.getId()).withSelfRel());
        list.add(u1);
        User u2 = new User();
        u2.setId(2);
        u2.setUsername("itboy");
        u2.setAddress("www.itboyhub.com");
        u2.add(WebMvcLinkBuilder.linkTo(UserController.class).slash(u2.getId()).withSelfRel());
        list.add(u2);
        CollectionModel<user> users = CollectionModel.of(list);
        users.add(WebMvcLinkBuilder.linkTo(UserController.class).withRel("users"));
        return users;
    }
}

关于这个接口,我来说几点:

  1. 首先,对于这种返回一个集合或者数组的情况,返回的类型都是 CollectionModel<user>。
  2. 把集合弄好之后(正常应该去数据库中查询,我这里省事直接创建了),通过 CollectionModel.of(list) 方法去获取一个 CollectionModel<user> 对象。
  3. 对于每一个 user 对象,我都添加了一个 Link 对象,WebMvcLinkBuilder.linkTo(UserController.class).slash(u1.getId()).withSelfRel() 表示生成当前对象的访问链接。
  4. WebMvcLinkBuilder.linkTo(UserController.class).withRel("users") 表示访问所有数据的链接。

好了,这个接口写完之后,我们访问看下:

可以看到,返回的每一个 user 对象中,都有一个链接表示如何单独访问这个对象。最下面还有一个访问所有对象的链接。

对于上面这个案例,可能有小伙伴会质疑,难道我们从数据库中查询出来的 List 集合都要遍历一遍,然后给每一个 User 添加一个 Link 吗?其实不必,添加 Link 这个事可以直接在 User 类中完成,如下:

public class User extends RepresentationModel {
    private Integer id;
    private String username;
    private String address;

    public User(Integer id) {
        super(WebMvcLinkBuilder.linkTo(UserController.class).slash(id).withSelfRel());
        this.id = id;
    }
    //省略 getter/setter
}

可以看到,直接在构造方法中完成即可。此时接口里就不用那么复杂了,如下:

@GetMapping
public CollectionModel<user> list() {
    List<user> list = new ArrayList&lt;&gt;();
    User u1 = new User(1);
    u1.setUsername("javaboy");
    u1.setAddress("www.javaboy.org");
    list.add(u1);
    User u2 = new User(2);
    u2.setUsername("itboy");
    u2.setAddress("www.itboyhub.com");
    list.add(u2);
    CollectionModel<user> users = CollectionModel.of(list);
    users.add(WebMvcLinkBuilder.linkTo(UserController.class).withRel("users"));
    return users;
}

那么对于根据 ID 来查询用户的需求,我们也应该给一个接口如下:

@RestController
@RequestMapping("/users")
public class UserController {

    @GetMapping("/{id}")
    public EntityModel<user> getOne(@PathVariable Integer id) throws NoSuchMethodException {
        User u = new User(id);
        u.setUsername("javaboy");
        u.setAddress("深圳");
        u.add(Link.of("http://localhost:8080/users/"+id, "getOne"));
        Link users = WebMvcLinkBuilder.linkTo(UserController.class).withRel("users");
        u.add(users);
        Link link = WebMvcLinkBuilder.linkTo(UserController.class).slash(u.getId()).withSelfRel();
        u.add(link);
        Method method = UserController.class.getMethod("getOne", Integer.class);
        Link link2 = WebMvcLinkBuilder.linkTo(method, id).withSelfRel();
        u.add(link2);
        return EntityModel.of(u);
    }
}

关于这个接口,我说如下几点:

  1. 如果返回类型是一个对象的话,需要使用 EntityModel<user> 类型。
  2. 搞好返回的对象之后,通过 EntityModel.of(u) 方法可以获取到目标数据类型。
  3. 这个地方,为了给小伙伴们演示不同的 Link 添加方式,我写了好多个(单纯为了演示不同的 Link 添加方式):
    1. Link.of("http://localhost:8080/users/"+id, "getOne") 这种是自己纯手工去生成当前对象的访问链接,很明显这不是一个很好的方案。当前对象的访问链接建议使用上文中提到的方式。
    2. WebMvcLinkBuilder.linkTo(UserController.class).withRel("users") 这个是生成当前这个 Controller 的访问链接,一般就是访问所有用户对象的链接。
    3. WebMvcLinkBuilder.linkTo(UserController.class).slash(u.getId()).withSelfRel() 前文已经用过了,不多说了,实际应用中建议使用这种。
    4. 也可以根据某一个方法自动生成,像这样 WebMvcLinkBuilder.linkTo(method, id).withSelfRel(),这个是生成某一个具体方法的访问链接。

好了,现在我们来看下这个接口生成的 JSON,如下:

生成的这段 JSON 我将之标记为了三部分:

  1. 第一部分,self,就是自身的访问链接,这三个链接分别是 User 的构造方法,以及前面提到的 3.3 和 3.4 的方法生成的。
  2. 第二部分,getOne 这个,是前面 3.1 中提到的方法生成的。
  3. 第三部分,users 这个,是前面提到的 3.2 方法生成的。

当然,其实这块还有很多其他的生成链接的玩法,但是我就不一一介绍了。

从上面 Spring HATEOAS 中返回的 JSON 我们大致上可以看到它的特点:

> 当我们使用了 Spring HATEOAS,此时,客户端就会通过服务端返回的 Link Rel 来获取请求的 URI(如果没有使用 Spring HATEOAS,则客户端访问的 URI 都是提前在客户端硬编码的),现在我们就可以做到服务端在不破坏客户端实现的情况下动态的完成 URI 的修改,从而进一步解耦客户端和服务端。

简而言之,现在客户端能干什么事情,在服务端返回的 JSON 中都会告诉客户端,客户端从服务端返回的 JSON 中获取到请求的 URL,然后直接执行即可。如果这个请求地址发生变化的话,客户端也会及时拿到最新的地址。

可能上面的例子小伙伴们感受还不是很明显,我再给大家看一段 JSON:

{
    "tracking_id": "666",
    "status": "WAIT_PAYMENT",
    "items": [
        {
            "name": "book",
            "quantity": 1
        }
    ],
    "_Links": {
        "self": {
            "href": "http://localhost:8080/orders/666"
        },
        "cancel": {
            "href": "http://localhost:8080/orders/666"
        },
        "payment": {
            "href": "http://localhost:8080/orders/666/payments"
        }
    }
}

这是电商系统下单之后等待支付的过程中返回的 JSON,这里的 links 给出了三个:

  • self:访问这个链接可以查看当前订单信息(GET 请求)。
  • cancel:访问这个链接可以取消当前订单(DELETE 请求)。
  • payment:访问这个链接可以支付当前订单(POST 请求)。

这个例子就很直白了,就是在返回的 JSON 中,直接告诉你接下来能做哪些操作,对应的 URL 分别是什么,前端拿到之后直接操作,如果这些操作路径发生了变化,前端也会立马拿到最新的路径。

这就是 Spring HATEOAS 的好处。总之一句话,Spring HATEOAS 提倡在响应返回的 Link 中给出对该资源接下来操作的 URL。这种方式解耦了服务端 URI,也可以让客户端开发者更容易地探索 API。

3. REST 的优缺点

虽然我们现在都鼓励设计 REST 风格的 API,然而 REST 也不全是优点,事物总是具有两面性,REST 的优缺点分别如下。

3.1 优点

  1. 首先,REST 足够简单,有一定 Web 开发经验的小伙伴都可以快速上手 REST。
  2. REST 风格的接口测试起来也非常方便,利用浏览器自带的一些 REST 插件或者是 POSTMAN 之类的工具,就可以非常方便的实现 REST 接口的测试。
  3. 不需要中间代理,简化了系统的结构。
  4. HTTP 对防火墙比较友好。

3.2 缺点

  1. REST 只支持请求 - 响应的通信方法,不支持服务端推送消息到客户端。
  2. 给请求取一个合适的名字比较困难,特别是有多个相类似的接口时,例如有多个添加接口、多个更新接口等。
  3. 由于没有中间代理,所以请求 / 响应的时候,服务端和客户端都必须在线。
 类似资料: