Spring HATEOAS:方便创建满足HATEOAS规则的Spring REST应用的库。这样的REST应用,返回的资源满足HAL规范,不止包含了data,还包含了对相关资源的link,因此能在很大程度上解耦服务器端与客户端代码。
举个例子,请求以下资源:
http://localhost:8080/greeting
附带Hypermedia元素(link)的 JSON格式的响应资源类似这样:
{
"content":"Hello, World!",
"_links":{
"self":{
"href":"http://localhost:8080/greeting?name=World"
}
}
}
该模型把 REST 服务按照成熟度划分成 4 个层次:
第一个层次(Level 0)的 Web 服务只是使用 HTTP 作为传输方式,实际上只是远程方法调用(RPC)的一种具体形式。SOAP 和 XML-RPC 都属于此类。
第二个层次(Level 1)的 Web 服务引入了资源的概念。每个资源有对应的标识符和表达。
第三个层次(Level 2)的 Web 服务使用不同的 HTTP 方法来进行不同的操作,并且使用 HTTP 状态码来表示不同的结果。如 HTTP GET 方法来获取资源,HTTP DELETE 方法来删除资源。
第四个层次(Level 3)的 Web 服务使用 HATEOAS。在资源的表达中包含了链接信息。客户端可以根据链接来发现可以执行的动作。
HATEOAS 是 REST 架构风格中最复杂的约束,也是构建成熟 REST 服务的核心。它的重要性在于打破了客户端和服务器之间严格的契约,使得客户端可以更加智能和自适应,而 REST 服务本身的演化和更新也变得更加容易。
从上述 REST 成熟度模型中可以看到,使用 HATEOAS 的 REST 服务是成熟度最高的,也是推荐的做法。对于不使用 HATEOAS 的 REST 服务,客户端和服务器的实现之间是紧密耦合的。客户端需要根据服务器提供的相关文档来了解所暴露的资源和对应的操作。当服务器发生了变化时,如修改了资源的 URI,客户端也需要进行相应的修改。而使用 HATEOAS 的 REST 服务中,客户端可以通过服务器提供的资源的表达来智能地发现可以执行的操作。当服务器发生了变化时,客户端并不需要做出修改,因为资源的 URI 和其他信息都是动态发现的。
HATEOAS 的核心是链接。链接的存在使得客户端可以动态发现其所能执行的动作。
打开eclipse,建立maven工程,pom.xml如下:
<project
xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<groupId>org.springframeworkgroupId>
<artifactId>gs-hateoas-serviceartifactId>
<version>0.0.1-SNAPSHOTversion>
<packaging>jarpackaging>
<properties>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
<java.version>1.8java.version>
properties>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.0.0.M5version>
parent>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-hateoasartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>com.jayway.jsonpathgroupId>
<artifactId>json-pathartifactId>
<scope>testscope>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
<repositories>
<repository>
<id>spring-milestonesid>
<name>Spring Milestonesname>
<url>https://repo.spring.io/libs-milestoneurl>
<snapshots>
<enabled>falseenabled>
snapshots>
repository>
repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-milestonesid>
<name>Spring Milestonesname>
<url>https://repo.spring.io/libs-milestoneurl>
<snapshots>
<enabled>falseenabled>
snapshots>
pluginRepository>
pluginRepositories>
project>
创建资源表现模型。Spring HATEOAS提供了一个基类ResourceSupport
,允许我们添加Link
对象,以返回符合HAL规范的JSON结果。
// src/main/java/hello/User.java
package hello;
import org.springframework.hateoas.ResourceSupport;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
public class User extends ResourceSupport {
private Integer uid;
private final String code;
private final String name;
@JsonCreator
public User(@JsonProperty("id") Integer uid, @JsonProperty("code") String code, @JsonProperty("name") String name) {
this.uid = uid;
this.code = code;
this.name = name;
}
/**
* @return the code
*/
public String getCode() {
return code;
}
/**
* @return the name
*/
public String getName() {
return name;
}
/**
* @return the id
*/
public Integer getUid() {
return uid;
}
}
// src/main/java/hello/UserList.java
package hello;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.springframework.hateoas.ResourceSupport;
public class UserList extends ResourceSupport {
private List list;
public UserList(Collection users) {
this.list = new ArrayList();
this.list.addAll(users);
}
/**
* @return the code
*/
public List getUserList() {
return list;
}
}
接下来是 控制器等
// src/main/java/hello/UserController.java
package hello;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.methodOn;
import java.util.Arrays;
import javax.websocket.server.PathParam;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class UserController {
@RequestMapping(method = RequestMethod.GET, value = "/user/list")
public HttpEntity list(@RequestParam(value = "name", required = false) String name) {
User[] users = { new User(1, "jack", "jack T"), new User(2, "tom", "tom C") };
UserList list = new UserList(Arrays.asList(users));
list.add(linkTo(methodOn(UserController.class).list(name)).withSelfRel());
list.add(linkTo(methodOn(UserController.class).get("")).withRel("get"));
list.add(linkTo(methodOn(UserController.class).insert(null)).withRel("new"));
list.add(linkTo(methodOn(UserController.class).update("")).withRel("update"));
return new ResponseEntity(list, HttpStatus.OK);
}
@RequestMapping(method = RequestMethod.GET, value = "/user/{id}")
public HttpEntity get(@PathParam(value = "uid") String uid) {
User user = new User(1, "jack", "jack T");
user.add(linkTo(methodOn(UserController.class).get(uid)).withSelfRel());
user.add(linkTo(methodOn(UserController.class).insert(null)).withRel("new"));
user.add(linkTo(methodOn(UserController.class).update(uid)).withRel("update"));
user.add(linkTo(methodOn(UserController.class).list("")).withRel("collection"));
return new ResponseEntity(user, HttpStatus.OK);
}
@RequestMapping(method = RequestMethod.PUT, value = "/user/new")
@ResponseStatus(HttpStatus.CREATED)
public HttpEntity insert(@RequestBody User auser) {
String code = auser.getCode();
String name = auser.getName();
User user = new User(1000, code, name);
user.add(linkTo(methodOn(UserController.class).insert(null)).withSelfRel());
user.add(linkTo(methodOn(UserController.class).get("")).withRel("get"));
user.add(linkTo(methodOn(UserController.class).update("")).withRel("update"));
user.add(linkTo(methodOn(UserController.class).list("")).withRel("collection"));
return new ResponseEntity(user, HttpStatus.OK);
}
@RequestMapping(method = RequestMethod.PATCH, value = "/user/{id}")
public HttpEntity update(@PathParam(value = "uid") String uid) {
User user = new User(1, "jack", "jack T");
user.add(linkTo(methodOn(UserController.class).update(uid)).withSelfRel());
user.add(linkTo(methodOn(UserController.class).get(uid)).withRel("get"));
user.add(linkTo(methodOn(UserController.class).insert(null)).withRel("new"));
user.add(linkTo(methodOn(UserController.class).list("")).withRel("collection"));
return new ResponseEntity(user, HttpStatus.OK);
}
}
最后是启动类
// src/main/java/hello/App.java
package hello;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
/**
* Hello world!
*/
@SpringBootApplication
public class App extends SpringBootServletInitializer {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}
maven spring-boot:run 启动服务端
使用curl访问服务器:
$ curl http://localhost:8080/user/list
{
"userList" : [ {
"id" : 1,
"code" : "jack",
"name" : "jack T"
}, {
"id" : 2,
"code" : "tom",
"name" : "tom C"
} ],
"_links" : {
"self" : {
"href" : "http://localhost:8080/user/list{?name}",
"templated" : true
},
"get" : {
"href" : "http://localhost:8080/user/{id}",
"templated" : true
},
"new" : {
"href" : "http://localhost:8080/user/new"
},
"update" : {
"href" : "http://localhost:8080/user/{id}",
"templated" : true
}
}
}
$ curl -X PUT -H 'content-type: application/json' -d '{"code":"TestAddCode"}' http://localhost:8080/user/new
{
"id": 1000,
"code": "TestAddCode",
"name": null,
"_links": {
"self": {
"href": "http://localhost:8080/user/new"
},
"get": {
"href": "http://localhost:8080/user/{id}",
"templated": true
},
"update": {
"href": "http://localhost:8080/user/{id}",
"templated": true
},
"collection": {
"href": "http://localhost:8080/user/list?name="
}
}
}
spring-boot-starter-data-jpa
spring-boot-starter-data-rest
spring hateoas项目
http://projects.spring.io/spring-hateoas/
spring guides
https://spring.io/guides/gs/rest-hateoas/
使用 Spring HATEOAS 开发 REST 服务
https://www.ibm.com/developerworks/cn/java/j-lo-SpringHATEOAS/index.html
REST 成熟度模型
https://www.crummy.com/writing/speaking/2008-QCon/act3.html
HAL规范
http://stateless.co/hal_specification.html