在如今的互联网时代,Web服务随处可见,REST这个词也经常出现。特别是现在微服务架构和前后端分离架构日益盛行,服务间越来越多的采用RESTful API通信,Java这门语言更是在Java SE 5中制定了JAX-RS(Jakarta/Java RESTful Web Services)规范,提供了一系列注解帮助开发者方便的构建符合RESTful风格的Web服务,当然REST和语言是没有直接关系的。但我发现很多人其实没有特别好的理解REST,或者说号称自己设计的是RESTful API,但实际一点都不RESTful。所以本文从偏应用的角度分享一些最佳实践和我自己的一些观点。

什么是REST

首先来看什么是REST?以下是维基百科的解释:

中文版:

表现层状态转换(英语:Representational State Transfer,缩写:REST)是Roy Thomas Fielding博士于2000年在他的博士论文中提出来的一种万维网软件架构风格,目的是便于不同软件/程序在网络(例如互联网)中互相传递信息。表现层状态转换是根基于超文本传输协议(HTTP)之上而确定的一组约束和属性,是一种设计提供万维网络服务的软件构建风格。符合或兼容于这种架构风格(简称为 REST 或 RESTful)的网络服务,允许客户端发出以统一资源标识符访问和操作网络资源的请求,而与预先定义好的无状态操作集一致化。

英文版:

Representational state transfer (REST) is a software architectural style that defines a set of constraints to be used for creating Web services. Web services that conform to the REST architectural style, called RESTful Web services, provide interoperability between computer systems on the Internet. RESTful Web services allow the requesting systems to access and manipulate textual representations of Web resources by using a uniform and predefined set of stateless operations. Other kinds of Web services, such as SOAP Web services, expose their own arbitrary sets of operations.

从这些解释里面我们能找出一些关键点:

  • REST只是一种软件架构风格,它不是标准,更不是协议。就像我们说的OOP一样,它只是一种编程思想和风格,我们不能因为Java是纯面向对象语言,就认为所有Java写的程序就一定是面向对象的,现实中很多Java代码其实就是面向过程风格的(注:面向过程和面向对象本身并没有孰优孰劣,尺有所短寸有所长而已)。
  • REST主要是基于HTTP协议,且应用于Web服务(Web Service),遵从REST风格的Web服务可以称之为RESTful Web services。
  • REST并非唯一的Web风格,主流的还有SOAP(Simple Object Access Protocol,简单对象访问协议)、XML-RPC。相比于后两者,REST比较简洁。

因为REST是出自Roy Thomas Fielding(也是HTTP 1.1协议的设计者之一)的博士论文,所以比较偏理论,原始论文读起来有些味同嚼蜡(个人感觉)。对绝大多数人来说肯定是没必要去研究论文的,根据二八原则,我们只要掌握百分之二十的REST知识,就能让自己构建的Web Services比较RESTful了。本着简洁的原则,本文就不写REST定义的架构属性和约束了,有兴趣的可以直接去看维基百科。

虽然RESTful API并没有强制使用HTTP+JSON的方式,但绝大部分的RESTful API都是基于HTTP+JSON这种形式的,所以本文就以这种讨论。另外,前面也说了,RESTful是一种软件架构风格,虽然定义了一些约束,但也很泛,导致在一些细节设计上没有特别统一的标准规范,所以本文下面分享的一些东西只是最佳实践和我本人的观点,并不表示你一定要这么做,或者REST明确规定你就要这么做,我们更多的是讨论如何让自己的API设计的更加RESTful而已。

如何设计RESTful API

设计比较RESTful的API有非常多的细节要注意,我认为有3个点尤其重要:URI的设计、HTTP方法的选择、Response的设计。做好这3个,我个人认为已经基本比较RESTful了。

URI设计

URI全称Uniform Resource Identifier,一般翻译为统一资源标识符(URL也可以视为一种URI)。REST规定所有的Web资源都必须通过一个URI进行访问,所以URI的设计至关重要。REST API中的URI的组成一般为:

http(s)://[主机名]:[端口]/[路径]?[查询参数]

主机名、端口都是配置的,需要设计的主要是路径以及参数(包括query、form-data等)。路径以及参数设计一般要考虑两方面:名称、层次。

名称

和变量起名类似,URI中的名称一般有3种命名:

  • 蛇形命名法:全小写加下划线,比如user_id.
  • 脊柱命名法:全小写加中划线,比如user-id.
  • 驼峰命名法:大小写混合,比如userId.

这三种里面不推荐使用驼峰命名法,最主要的原因是URI中协议和主机名(即域名)是不区分大小写的,比如下面几种效果是一样的:

但除协议和主机名之外,其它地方都是区分大小写的,比如https://niyanchun.com/pages/about.htmlhttps://niyanchun.com/pages/ABOUT.html理论上是不一样的。这里说理论上不一样是因为RFC 3986中是这样定义的,但实际中有些服务器端的实现可能是不区分的(大部分对外的网站的网址为了方便用户,都是不区分大小写的),但我们不能依赖这个,我们必须假定实际的实现是区分大小写的。

另外一个原因是像一些FTP或者静态资源的Web,他们的资源来自于操作系统(准确的说是操作系统的文件系统),比如Windows就是不区分大小写的,“a.pdf”和“A.pdf”是一样的,但Linux等unix-like的系统是区分大小写的,“a.pdf”和“A.pdf”是不一样的。

所以基于这些原因,在设计路径以及参数的名称的时候建议使用蛇形命名法或者脊柱命名法(推荐),但不要混用。个人建议脊柱命名法,因为它的输入比较方便,而下划线输入的时候还要按shift键,尤其在移动设备上很不方便。还有一个就是下划线在某些显示场景下容易和下边沿重合,看着不清楚。这里需要额外说明的是蛇形命名法或者脊柱命名法并没有规定一定要使用小写字母,它们仅规定使用中划线或者下划线来连接单词,但因为我们不建议在URI中使用大写字母,所以就直接规定为全小写字母了。

注意:这里说的命名规则指的是URI中的非变量部分,比如对于URI:http://example.com/orders/{order_id},其中的order_id是path parameter的占位符,会被最终的实际id替换掉,而且这部分一般对应的是代码中定义的变量,这部分不使用脊柱命名法,一般和你的变量命名风格统一。

另外,在REST中,URI中要使用名词,而非动词(SOAP风格里面推荐使用动词,比如getBookById)。具体的行为应该由HTTP方法去体现。有一个例外是当行为太多,HTTP方法不够用时,有时会在URI最后面加一个动词(一般称为controller),这个地方是唯一可以使用动词的地方。比如下面这个:

POST /alerts/245743/resend

但是,设计良好的URI应该极少有这种场景,如果你发现你的设计中有很多这种,很可能你的设计本身存在问题。

层次

说到层次,先说一个比较简单的规范,就用被玩烂了的客户(customer)举例吧:

  • 单个客户增删改查的URI:http://example.com/customer/{id}
  • 批量客户增删改查URI:http://example.com/customer

实际中对于API,一般还会加上一些统一的前缀,比如版本、模块等,这里就不讨论了。别看这两个简单的URI,现实中很多RESTful API都没有做到,访问具体某一个资源的时候,要把id等资源标识符放在path上面,而不是放在querystring或者form-data等数据里面;访问一组资源的时候,路径到资源就可以了,最后面不要加/

然后就是分层次了,先看个简单的例子,比如客户下面还会有订单(order),那订单这个资源的URI该如何设计呢?看下面的一些例子(为了方便,这里只举单个订单资源的URI的例子,批量的去掉最后面的id即可):

  1. http://example.com/orders/{order_id}
  2. http://example.com/{customer_id}/{order_id}
  3. http://example.com/customers/orders/{order_id}
  4. http://example.com/{customer_id}/orders/{order_id}
  5. http://example.com/customers/{customer_id}/orders/{order_id}

上面5种,哪种设计更好呢?It depends...上面5种URI越来越长,包含的层级信息越来越多。选择多了有时候未必是好事,特别是选项特别多的时候就一定不是一件好事了。这里我也没法给出孰优孰劣,只能给一些方法,然后每个人根据自己的情况去做取舍了。我列两种比较极端的处理方式:

  1. 最短原则:即上面的1. 其实按照REST的设计,分客户端(Client)和服务端(Server),资源的层次结构是由服务端去自由控制的,不需要体现给客户端。如果单从这个角度考虑的话,其实1这种方式是完全OK的,是更加RESTful的。
  2. 最长原则:实际中我们往往在URI的设计上一般还是会体现一下层级关系,上面例子中只有Customer和Order两级关系,但业务往往是复杂的,可能会有三级、四级,甚至更多,还可能是嵌套的。总的原则就是不建议在URI上面嵌套超过2级的层次关系,那样会让URI很长。

现实中,往往是在最长和最短中间取一个折中的选择(成功的又把问题抛出去了~~)。

单数or复数

这个问题是一个容易引起无休止讨论的问题,且没有标准答案,StackOverflow就有一个讨论这个的问题:REST URI convention - Singular or plural name of resource while creating it,有兴趣的可以查看。对于URI中资源使用单数还是复数,我的个人观点是:不论是单数还是复数,统一就好:所有资源保持一致,操作单个资源和批量资源保持一致。如果一定要在单数和复数里面选一个,我个人倾向于选择全部采用复数形式。

这里请务必注意,我们讨论的仅仅是URI中的资源部分,而不是其它部分,比如假设有个服务是用户管理,然后你的API是这样的:

http://example.com/user-management/users/{id}

那其中的user-management指代的是服务,不算资源,后面的users才是资源。非资源部分一般推荐使用单数形式。

HTTP方法选择

这部分非常简单,前面说了,URI中不包含动词,具体行为是有HTTP方法描述的,而用来对资源进行CRUD的方法无外乎以下四种:

  • GET:查询操作。特点是安全且幂等。安全是指不会对服务端资源进行任何修改,幂等是指多次执行产生的结果是一样的。
  • POST:创建新资源。不安全、不幂等。
  • PUT:更新或创建新资源。不安全、幂等。
  • DELETE:删除资源。不安全、幂等。

当然HTTP还有其它方法,但在RESTful API中使用很少,这里不讨论了。这里有两个问题需要注意。

一个是有一个特殊情况就是如果查询的参数非常多,或者有敏感数据,可以使用POST实现查询,比如很多DSL查询都走的是POST,但也有走GET的,比如ElasticSearch的DSL查询同时支持GET和POST。

另外一个是上面有个细节不知道大家注意到没有:POST和PUT都能用于创建新资源,那有什么区别呢?

当客户端能够决定资源最终的URI时(其实就是决定资源的唯一标识符时),可以使用PUT;如果不能,就使用POST。比如我们要创建一个新的订单,如果客户端可以决定(需要服务端支持)订单ID,那可以使用PUT操作:

PUT http://example.com/orders/1

如果客户端决定不了,需要服务端自己生成,那就使用POST:

// 格式1
POST http://example.com/orders
// 格式2
POST http://example.com/orders/1

注意一下上面的格式2,也是有这种形式的,此时那个1仅是客户端给服务端的建议,服务端未必会采纳。

所以,为了让你的API更加RESTful,请只在客户端能够决定资源ID的时候,才使用PUT创建新资源,或者表达“不存在就创建,存在就更新”的语义的时候才使用PUT操作,否则请使用POST创建新资源。

下面给一些例子:

// 查询所有订单
GET http://example.com/orders
// 查询某个订单
GET http://example.com/orders/{id}
// 创建一个或多个订单(如果有批量创建的需求,可以分两个URI。个人建议使用一个URI,然后支持1~n个资源对象)
POST http://example.com/orders
// 修改一个订单
PUT http://example.com/orders/{id}
// 修改多个订单
PUT http://example.com/orders/{id1},{id2},{id3}
// 删除一个订单
DELETE http://example.com/orders/{id}
// 删除多个订单
DELETE http://example.com/orders/{id1},{id2},{id3}
// 删除所有订单
DELETE http://example.com/orders

Response设计

这个其实也很灵活,这里仅讨论两个问题:

  • 返回的HTTP状态码
  • 返回的Body格式是否要统一

返回的HTTP状态码

REST对于Response返回的HTTP状态码其实就一个要求:状态码和行为是一致的。要判断状态码和行为是否一致首先肯定要知道HTTP状态码的含义。HTTP规定的状态码说多不多,但说少也不少,我相信极少有人能完整说出HTTP规范定义的每个状态码及其含义。好在常用的状态码并不多,RESTful API中常用的就更少了(而且也不建议用太多)。常用的有以下这些状态码:

  • 表示成功的:即2xx类的,RESTful API中最常用的也就200和201了。200表示通用的成功,201表示新建资源成功,POST接口创建新资源一般返回201。
  • 表示重定向类的:即3xx类的,这种在RESTful API中很少用,最多也就用301。一般3xx的返回值算成功,客户端需要根据3xx返回的重定向地址重新发起强求,RESTful API中一般很少用重定向技术,这里就不说了。
  • 表示客户端错误的:即4xx类的,常用的有400、401、403、404. 400表示通用客户端错误,比如参数非法等;401表示未认证,比如没有传用户名、密码或者传的不对等;403表示未授权,这个注意要和401区分一下:401是未认证,403是已经认证过了,但是没有权限访问这个资源;404表示资源未找到。
  • 表示服务端错误的:即5xx类的,绝大多数RESTful API里面只使用通用的500即可,即表示服务端内部错误。

注意一下,现在我们开发一个RESTful API肯定会基于某个HTTP框架去开发,比如Java的Spring MVC、Jersey,Python的Flask,Golang的Beego等,那这个时候Response大的有两种:一种是我们应用层代码返回的,一种是框架层返回的。上面说的这些常用的HTTP状态码仅指应用层返回的情况,也就是我们能决定的情况。而框架一般会实现的比较完善,可能会根据不同的情况返回一些比较偏的HTTP状态,但服务端无需关注。

了解了HTTP常用的状态码含义,再来看什么叫状态码和行为一致。所谓一致就是请求成功了,肯定返回的状态码是2xx的,你返回个4xx、5xx那肯定就是不符合REST规范了;同理,明明业务失败了,你还返回2xx,那肯定也是不一致的,而且明明是未授权,你却返回一个500,那也不一致。现在问题来了:

  1. 为什么需要一致?
  2. HTTP状态码那么少,不足以描述复杂的业务场景怎么办?

第1个问题:为什么要一致?两个原因,其一,定标准、定规范的目的就是为了统一,为了减少沟通的成本。在你的业务里面,你把2xx定义为失败,5xx定义为成功,技术上可以做到,但这样你就得给团队里面每个人都讲一下你的这个规则;换一个人,你都得再讲一遍。更重要的,还会被懂行的人鄙视,内心一万个xxx在蹦腾。其二,REST和HTTP是非常密切的(都是一个作者),REST里面的一些思想已经被定义在了HTTP规范里面,比如缓存。一般GET请求如果返回200,查询结果是会被缓存的(当然实际得看服务端的实现以及查询的参数、条件等),如果数据没有变化,会直接读上次查询的缓存,这对于查询操作比较重的情况还是能提升不少性能的,也能减少服务器的压力,类似的道理还有很多。如果你一个查询操作非要使用POST,或者明明成功了,却要返回5xx,那这些优势就没了。而且还可能会产生一些错误,后文会提到。

第2个问题:HTTP状态码那么少,不足以描述复杂的业务场景怎么办?这个问题也简单,很多人都知道答案,那就是自定义业务状态码,的确是这样的。但是一定要注意的是自定义的业务状态码只是HTTP状态码的一个补充,二者一定要是一致的。从技术上来说HTTP状态码是HTTP协议的一部分,而自定义的业务状态码只是自定义格式的Body的一部分而已,谁也不是谁的替代品。不能因为自定义了业务状态码,就忽略HTTP状态码与业务行为的一致性了,比如明明失败了,还返回个200,哪怕你的业务状态码已经表明产生了错误。弊端下面会再次提到。

返回的Body格式是否要统一

这个问题其实已经不是REST的范畴了,只是在设计RESTful API的时候,常常会面临这个问题,所以这里也拿出来讨论一下。这个问题呢是这样的:就是说API返回的Body的数据格式要不要在任何情况下都统一。目前常见的做法有三种:

  1. 正常、异常返回都不统一。
  2. 正常返回不统一,异常返回统一。
  3. 正常、异常返回都统一。

现在都是基于框架开发RESTful API,第1种情况已经比较少见了,至少框架层返回的错误格式一般都是统一的,如果你自己在应用层返回的不统一,那就是你自己的事情了,或者说你没用按照框架提供的方式去返回(即没有用正确的姿势使用框架)。

比较多的主要是第2种和第3种情况。这种问题没有标准答案,而且有时还是要结合业务以及具体使用的框架去看,以下为我个人观点。我一般倾向于使用第2种情况,主要两个原因:一,我使用过Python的Flask、Golang的Beego、Java的Spring MVC、Jersey等Web/JAX-RS框架,它们默认都没有对正常返回时的数据格式做统一封装,甚至有的接口返回列表,有的返回JSON,但对于异常情况都是统一了返回格式,至少都提供了统一封装异常返回的方式。二,很多选用3的人的观点是所有情况统一了数据结构,前端好处理,但这个真的是理由吗?我认为不是的,即使你统一了大格式,里面存储具体格式的字段还是统一不了。举一个下面的例子:

{
    "status": "xxx",
    "error_code": "xxxx",
    "message": "xxxx",
    "data": "xxx"
}

上面这个是一个比较简单的统一格式,data字段用于存返回的业务数据,现在即使所有情况都统一成这种格式,不同的接口,data字段对应的value也是很难统一的,有的可能仅返回一个字符串/布尔值/整数值,有的可能返回一个list,有的返回一个map,代码层面你可以定义成Object(以Java为例),但前端人员还是要去看接口文档,然后根据情况做必要的反序列化或者类型转换,和直接返回data的value没有太多区别。

但异常情况我认为是非常有必要统一的,上面说的正常情况不统一是因为正常情况下数据格式是确定的,但异常情况却是不确定的,甚至有些异常根本就到不了应用层代码,比如url写错这种一般框架层就会返回404,根本到不了应用层代码。也就是异常很不可控,我们没法像正常情况那样事先列举好的每一种异常情况,但前端需要一个这样的东西。所以可以统一异常时的返回形式,如果返回的状态码不是2xx,就按约定的统一的异常结构进行解析。然后这个统一的异常结构里面一般都会存放状态、简单的错误原因、详细的错误信息(比如调用栈等)、自定义错误码等,这个就是结合自己的业务来了。所以至于2和3如何选,我一般建议是看你用的框架,如果他默认统一,那就统一;如果不统一,那就不统一,但对于异常情况一定要统一。

但第3种设计还有一种情况就是不论业务语义是成功还是失败,HTTP状态码都返回200,实际成功还是失败需要解析Body才知道,这种情况的确有,但这种设计方式我认为是应该极力避免的,即使统一了正常、异常情况下的结构,即使你的结构里面已经包含了业务真实的状态,那也不要放弃保持HTTP状态码与业务语义的一致性,不然这种设计不光不RESTful,而且还很糟糕。一方面是语义不一致,和HTTP规范违背,另一方面前面也提到了,很多HTTP服务器会根据HTTP的状态去做一些优化,比如缓存技术,如果你不按规范来,轻则使用不到一些协议带来的优势,重则还会产生错误。比如明明业务失败了,但GET却返回了200,这个时候HTTP服务还把这个结果缓存了,那可能你后来修复了错误,但客户端依旧拿到的是缓存的错误。如果你说你的接口只是发了一个请求,实际的执行是异步的,得执行完之后才知道结果。这种情况很常见,但HTTP是个同步操作协议,它的状态码仅标识这次这个同步请求自身是否成功,至于业务层的成功并不强制通过这个状态码体现。创建资源就是一个最好的例子,复杂系统创建一个资源往往是一个重操作,比如在YARN上提交一个任务,那YARN提交的REST接口返回成功并不表示这个任务就一定会成功创建,它的成功仅表示你请求创建任务这个请求服务器已经成功收到了而已。这就是201这个状态码存在的意义,我们发现有些创建资源返回的是200,有些是201,一部分原因是每个人对HTTP掌握程度不一,一部分原因是对于轻量级的资源创建,可能同步就创建完成了,所以返回201,表示资源已创建;有些创建是重操作,那可能返回仅表示服务端成功收到了你的请求,这个时候返回200,而非201也是可以接受的。

总结

本来想让文章简短一些,稍不注意就又写多了。所以这里做一下总结:如何才能让自己的API比较RESTful:

  • URI层级使用/分级,其它部分仅使用小写字母、数字、中划线三者的组合,且不要以数字、中划线开头;
  • URI中使用名词,不要使用动词,且名词都使用复数形式;动词在HTTP方法上面体现。
  • URI上可以体现资源的层级,但嵌套不要超过两级,尽量保持URI简短。
  • 任何情况下都应该保证HTTP动词的语义和业务语义一致,比如查询使用GET,创建新资源使用POST,修改资源使用PUT,删除资源使用DELETE。
  • 任何情况下都应该保证HTTP状态码与业务实际状态一致,比如成功就返回2xx,客户端失败就返回4xx,服务端失败就返回5xx。

一言以蔽之,好的RESTful API能达到这样的效果:看了URI就知道操作的是哪个资源;看了HTTP方法就知道执行了什么操作;看到返回的HTTP状态就知道执行是否成功。

推荐阅读资料

前面也说了,按照二八原则本文涉及的内容仅是REST的20%(实际可能还不到),要真正构建一个RESTful WebService,或者只是一个RESTful API,其实还是有很多细节需要关注。但如前文所说,也许REST的最大问题在于它仅是一个架构风格,没有明确的规范,特别是细节方面的规范,导致主观性因素很多。不过,总体还是有一些业界统一比较认可方法论可以作为参考,这里我补充一些不错的资源,有兴趣的可以查阅:

  • RESTful WebServices,Subbu Allamaraju著
  • RESTful WebServices Cookbook,InfoQ翻译整理的一个精简版本
  • REST in Practice, Jim Webber, Savas Parastatidis, Ian Robinson著
  • RESTful API guidelines,是一个起草中的RESTful API RFC规范

特别是最后一个,有一些明确规定的规范,值的好好读一下。

文章目录