说起 REST API,小伙伴们多多少少都有听说过,但是如果让你详细介绍一下什么是 REST,估计会有很多人讲不出来,或者只讲出来其中一部分。
今天松哥就来和大家一起来聊一聊到底什么是 REST,顺便再来看下 Spring HATEOAS 的用法。
首先关于 REST,有一个大佬 Leonard Richardson 为 REST 定义了一个成熟度模型,他一共定义了四个不同的层次,分别如下:
在日常的开发中,我们一般都是只实现到 Level2 这一层级,真正做到 Level3 的估计很少,不过虽然在工作中一般不会做到 Level3 这一层级,但是,我相信很多小伙伴应该是见过 Level3 层级的 REST 是啥样子的,特别是看过 vhr 视频的小伙伴,松哥在其中讲过,通过 Spring Data Jpa+Spring Rest Repositories 实现的 CURD 接口,其实就是一个达到了 Level3 层级的 REST。
那么接下来我先用 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<>();
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;
}
}
关于这个接口,我来说几点:
CollectionModel.of(list)
方法去获取一个 CollectionModel<user>
对象。WebMvcLinkBuilder.linkTo(UserController.class).slash(u1.getId()).withSelfRel()
表示生成当前对象的访问链接。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<>();
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);
}
}
关于这个接口,我说如下几点:
EntityModel<user>
类型。EntityModel.of(u)
方法可以获取到目标数据类型。Link.of("http://localhost:8080/users/"+id, "getOne")
这种是自己纯手工去生成当前对象的访问链接,很明显这不是一个很好的方案。当前对象的访问链接建议使用上文中提到的方式。WebMvcLinkBuilder.linkTo(UserController.class).withRel("users")
这个是生成当前这个 Controller 的访问链接,一般就是访问所有用户对象的链接。WebMvcLinkBuilder.linkTo(UserController.class).slash(u.getId()).withSelfRel()
前文已经用过了,不多说了,实际应用中建议使用这种。WebMvcLinkBuilder.linkTo(method, id).withSelfRel()
,这个是生成某一个具体方法的访问链接。好了,现在我们来看下这个接口生成的 JSON,如下:
生成的这段 JSON 我将之标记为了三部分:
当然,其实这块还有很多其他的生成链接的玩法,但是我就不一一介绍了。
从上面 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 给出了三个:
这个例子就很直白了,就是在返回的 JSON 中,直接告诉你接下来能做哪些操作,对应的 URL 分别是什么,前端拿到之后直接操作,如果这些操作路径发生了变化,前端也会立马拿到最新的路径。
这就是 Spring HATEOAS 的好处。总之一句话,Spring HATEOAS 提倡在响应返回的 Link 中给出对该资源接下来操作的 URL。这种方式解耦了服务端 URI,也可以让客户端开发者更容易地探索 API。
虽然我们现在都鼓励设计 REST 风格的 API,然而 REST 也不全是优点,事物总是具有两面性,REST 的优缺点分别如下。