到底什么样的 REST 才是最佳 REST?

说起 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 依赖,如下:

image

创建好之后,我们首先创建一个 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<>();
        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") 表示访问所有数据的链接。

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

image

可以看到,返回的每一个 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<>();
    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,如下:

image

生成的这段 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. 由于没有中间代理,所以请求/响应的时候,服务端和客户端都必须在线。

好啦,跟小伙伴们聊了 REST 和 Spring HATEOAS,感兴趣的小伙伴可以去试试哦~

?著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,128评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,316评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事?!?“怎么了?”我有些...
    开封第一讲书人阅读 159,737评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,283评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,384评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,458评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,467评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,251评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,688评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,980评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,155评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,818评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,492评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,142评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,382评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,020评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,044评论 2 352

推荐阅读更多精彩内容

  • 前言 REST是什么2.1、起源2.2、REST架构的标志2.3、超媒体(hypermedia)2.4、REST误...
    寒江雪_独钓阅读 1,183评论 0 7
  • 一说到REST,我想大家的第一反应就是“啊,就是那种前后台通信方式?!钡窃谝笙晗附彩鏊岢龅母鞲鲈际?,以及如...
    曹元_阅读 362评论 0 2
  • 简介 我们知道REST是一种架构方式,它只是指定了六种需要遵循的基本原则,但是它指定的原则都比较宽泛,我们需要一种...
    flydean程序那些事阅读 321评论 0 0
  • 简介 近几年微服务是如火如荼的在发展,而微服务之间的调用和渐渐的从RPC调用转移到了HTTP调用。于是经常听到有些...
    flydean程序那些事阅读 528评论 0 1
  • REST本身是一个高度抽象化的架构风格,因而总是很难对它有一个比较深入且印象深刻的理解。写这篇文章的目的,是自己对...
    vito1994阅读 2,843评论 0 26