不要被名字吓到:RESTful、HATEOAS、Spring boot之整合

My GitHub Home Page

原文地址:https://www.jianshu.com/p/65b9e54dee7d

最近在使用 Spring Boot 构建 RESTful 服务的时候遇到了一些不清楚的地方,查阅资料后写下此文,试图大致解释清楚什么是 RESTful,什么是正确的 RESTful,以及怎么在 Spring Boot 中定义和使用 RESTful。

什么是 RESTful

REST 这个词,是 Roy Thomas Fielding 在他 2000 年的博士论文中提出的。翻译过来就是 表现层状态转化

Fielding 在论文中将 REST 定位为 分布式超媒体应用 (Distributed Hypermedia System) 的架构风格,它在文中提到一个名为 HATEOAS (Hypermedia as the engine of application state) 的概念。

HATEOAS 又是什么鬼

我们知道 REST 是使用标准的 HTTP 方法来操作资源的,但仅仅因此就理解成带 CURD 的 Web 数据库架构就太过于简单了。这种说法忽略了一个核心概念:超媒体即应用状态引擎 (hypermedia as the engine of application state)

什么是超媒体

当你浏览 Web 网页时,从一个连接跳到一个页面,再从另一个连接跳到另外一个页面,就是利用了超媒体的概念:把一个个把资源链接起来

要达到这个目的,就要求在表述格式里边加入链接来引导客户端。在《RESTFul Web Services》一书中,作者把这种具有链接的特性成为连通性。

RESTful API 最好做到 Hypermedia 或 HATEOAS,即返回结果中提供链接,连向其他 API 方法,使得用户不查文档,也知道下一步应该做什么。

比如,当用户向 api.example.com 的根目录发出请求,会得到这样一个文档:

{"link": {
    "rel": "collection https://www.example.com/zoos",
    "href": "https://api.example.com/zoos",
    "title": "List of zoos",
    "type": "application/vnd.yourformat+json"
}}

上面代码表示,文档中有一个 link 属性,用户读取这个属性就知道下一步该调用什么 API 了:

  • rel:表示这个 API 与当前网址的关系(collection 关系,并给出该 collection 的网址)
  • href:表示 API 的路径
  • title:表示 API 的标题
  • type:表示返回类型

Hypermedia API 的设计被称为 HATEOAS。Github 的 API 就是这种设计,访问 api.github.com 会得到一个所有可用 API 的网址列表:

{
    "current_user_url": "https://api.github.com/user",
    "authorizations_url": "https://api.github.com/authorizations",
    // ...
}

从上面可以看到,如果想获取当前用户的信息,应该去访问 api.github.com/user,然后就得到了下面结果:

{
    "message": "Requires authentication",
    "documentation_url": "https://developer.github.com/v3"
}

以上内容都摘自阮一峰和其它作者博客,如有冒犯,请及时告知。

我当时第一眼看到 HATEOAS 也是一脸懵逼,因为在 Spring 依赖中看到过这个词,所以就留意了一下。其实在我看来,HATEOAS 是符合 RESTful 规范的一个方面,客户端在消费 RESTful 服务的时候,除了得到资源本身以外,还可以得到一些相关其他信息。比如,其他相关链接,返回类型等等。

构建 RESTful 服务最佳实践

第一条也是最容易犯错的:URI 中不应该包含动词。因为 资源 表示一种实体,所以应该是名词,URI 不应该有动词,动词应该放在 HTTP 协议中。

举例来说,某个URI是 /posts/show/1,其中 show 是动词,这个 URI 就设计错了,正确的写法应该是 /posts/1,然后用 GET 方法表示 show。

如果某些动作是 HTTP 动词表示不了的,你就应该把动作做成一种资源。比如网上汇款,从账户 1 向账户 2 汇款 500 元。

错误的 URI 是:

POST /accounts/1/transfer/500/to/2

正确的写法是把动词 transfer 改成名词 transaction,然后以参数的方式注明其它参数。

POST /accounts/transaction?from=1&to=2&amount=500.00

RESTful API 最好做到 Hypermedia (HATEOAS),即返回结果中提供链接,连向其他 API 方法,使得用户不查文档,也知道下一步应该做什么。

其它需要注意的地方参见文末贴出的链接。

下面重头戏来了:

使用 Spring Boot 构建符合 Hypermedia 规范的 RESTful 服务

我以后每次都要说一遍:Spring Boot 框架是所有 Java 开发者的福音。

在 Spring Boot 中构建符合 Hypermedia 规范的 RESTful 服务简单到不能再简单,只需要添加一条依赖:

<dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>

添加一个简单的领域类:

@Entity
public class Person {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;

    private String firstName;
    private String lastName;

    //getter and setter
}

以及一个 dao 层接口:

// @RepositoryRestResource(collectionResourceRel = "people",path="people")
public interface PersonRepository extends PagingAndSortingRepository<Person,Long> {
    List<Person> findByLastName(@Param("name") String name);
}

注释掉的标签可选,主要是在只用 RESTful 的时候可以改变 URI,比如,加上此处就把 /person 变成 /people

一切都和我们正常开发 web 没啥区别,但是现在,见证奇迹的时刻到了:

Step 1

GET localhost:8080

返回:

{
    "_links":{
        "people":{
            "href": "http://localhost:8080/people{?page,size,sort}",
            "templated": true
        },
        "profile":{
            "href": "http://localhost:8080/profile"
        }
    }
}

上面的返回中包括了 people 这个资源的链接明确指出了我们可以用类似 http://localhost:8080/people?page=1&size=10&sort=firstname 这样的方式请求资源。

Step 2

增加一个 people 资源:

POST localhost:8080/people

请求数据用 json

{"firstName": "李", "lastName": "雷"}

返回:

{
    "firstName": "李",
    "lastName": "雷",
    "_links":{
        "self":{
            "href": "http://localhost:8080/people/5"
        },
        "person":{
            "href": "http://localhost:8080/people/5"
        }
    }
}

返回信息中,除了新加入信息的各个字段,还有一个 href 链接指向它。这是合乎情理的,客户端总是想要看看新加入的这条信息长什么样,从这个角度说,这条返回信息还是很贴心的。

Step 3

GET localhost:8080/people/

返回:

{
    "_embedded":{
        "people":[
            {
                "firstName": "李",
                "lastName": "雷",
                "_links":{"self":{"href": "http://localhost:8080/people/5" }, "person":{"href": "http://localhost:8080/people/5"}
            }
        ]
    },
    "_links":{
        "self":{
            "href": "http://localhost:8080/people"
        },
        "profile":{
            "href": "http://localhost:8080/profile/people"
        },
        "search":{
            "href": "http://localhost:8080/people/search"
        }
    },
    "page":{
        "size": 20,
        "totalElements": 1,
        "totalPages": 1,
        "number": 0
    }
}

返回一个 people 列表,包含所有数据 page 标签的出现,是由于我们的 repository 继承了 PagingAndSortingRepository 接口。

search 标签的出现,是由于我们的 repository 声明了一个方法:

List<Person> findByLastName(@Param("name") String name);

这个方法可以像 search 标签描述的那样调用:

http://localhost:8080/people/search/findByLastName{?name}

示例:

http://localhost:8080/people/search/findByLastName?name=雷

Step 4

PUT localhost:8080/people/5

json

{"firstName": "李", "lastName" : "小雷"}

资源被正确更新

Step 5

PATCH localhost:8080/people/5

json

{"lastName": "大雷"}
{
    "firstName": "李",
    "lastName": "大雷",
    "_links":{
        "self":{
            "href": "http://localhost:8080/people/5"
        },
        "person":{
            "href": "http://localhost:8080/people/5"
        }
    }
}

资源也被更新了。

PUT 和 PATCH 有什么区别

PUT 相当于整体替换,也就是说,如果我把上面 PATCH 换成 PUT (我们注意到传过去的参数中没有 firstname),那么资源对象的 firstname 将为空:

{
  "firstName": null,
  "lastName": "大雷",
  ....
}

而 PATCH 则没有这个问题,只改该改的。

所以,数据库的 update 操作最好用 PATCH,但是这个方法也有一个问题,一些老旧浏览器不支持,什么时候用,自己权衡吧。

DELETE 就略了。

这篇文章到这里就收尾了?我相信各位看官和我一样,有一个奇怪的问题萦绕在心头。

  • where is controller?
  • where is controller?
  • where is controller?

从头至尾我们并没有写任何控制器,这不科学啊!

这恰恰体现了 RESTful 的思想,我们要的是资源,不是服务。所以我们只要规定好怎么获取资源就行了,其它的万能的 Spring Boot 已经帮我们做了,看到这里有没有对 Sping 有了森森的感激之情?

Spring 你这是要逆天啊,不用写三层,不用写业务逻辑也就完了,你现在连 controller 也不让我写了,作为一个程序猿我还有什么乐趣啊?

实际上这也是将来的趋势,现在无后端应用越来越多,比如基于 firebase 的,以后的后端程序猿不是写三层了,应该重点关注服务器性能优化、分布式计算方面,三层啊数据库啊这些事就交给框架自动去完成吧,保证又快又好。

参考文章: