Restful 风格接口命名及规则

一句话解释就是“通过路径知晓访问资源是何, 通过请求方式知道要做什么操作”。REST全称是表述性状态转移(Representational State Transfer),那究竟指的是什么的表述? 其实指的就是资源。任何事物,只要有被引用到的必要,它就是一个资源。资源可以是实体(例如手机号码),也可以只是一个抽象概念(例如价值) 。

大致遵循以下两个规则:

  • 请求 API 的 URL 表示用来定位资源;

  • 请求的 METHOD 表示对这个资源进行的操作;

REST提倡所有的接口都是基于资源的,所有的增删改查操作都是对于资源状态的改变

API的URL

通过URL用来定位资源,跟要进行的操作区分开,这就意味着URL不该有任何动词

  1. 下面示例中的 get、create、search 等动词,都不应该出现在 REST 架构的后端接口路径中。比如:
    • /api/getUser
    • /api/createApp
    • /api/searchResult
    • /api/deleteAllUsers
  2. 当我们需要对单个用户进行操作时,根据操作的方式不同可能需要下面的这些接口
    • /api/getUser (用来获取某个用户的信息,还需要以参数方式传入用户 id 信息)
    • /api/updateUser (用来更新用户信息)
    • /api/deleteUser (用来删除单个用户)
    • /api/resetUser (重置用户的信息)
  3. 可能在更新用户不同信息时,提供不同的接口,比如:
    • /api/updateUserName
    • /api/updateUserEmail
    • /api/updateUser

(以上是有状态的 URI)

以上三种情况的弊端在于:首先加上了动词,肯定是使 URL 更长了;其次对一个资源实体进行不同的操作就是一个不同的 URL,造成 URL 过多难以管理。

  1. 在 RESTful 风格的 URI 中则是
  • URL 中不应该出现任何表示操作的动词,链接只用于对应资源;

  • URL 中应该单复数区分,推荐的实践是永远只用复数;比如 GET /api/users 表示获取用户的列表;如果获取单个资源,传入 ID,比如 /api/users/123 表示获取单个用户的信息;

按照资源的逻辑层级,对 URL 进行嵌套,比如一个用户属于某个团队,而这个团队也是众多团队之一;那么获取这个用户的接口可能是这样:GET /api/teams/123/members/234 表示获取 id123 的小组下,id234 的成员信息。

按照类似的规则,可以写出如下的接口

  • /api/teams (对应团队列表)

  • /api/teams/123 (对应 ID123 的团队)

  • /api/teams/123/members (对应 ID123 的团队下的成员列表)

  • /api/teams/123/members/456 (对应 ID123 的团队下 ID456 的成员)

(RESTful 风格的 URI 是无状态的)

注意:其实当你回过头看“URL”这个术语的定义时,更能理解这一点。URL 的意思是统一资源定位符,这个术语已经清晰的表明,一个 URL 应该用来定位资源,而不应该掺入对操作行为的描述

两种风格的对比

有状态的 URI(传统) 无状态的 URI(RESTful)
GET /getUser?id=1 /user/1
GET /getUser?name=zhangsan&age=10 /user?name=zhangsan&age=10
POST /createUser /user
PUT /updateUser?id=1 /user/1
DELETE /deleteUser?id=1 /user/1
GET /getGroupUser?groupId=123&userId=456 /group/123/user/456

常见 RESTful 风格的 API 示例

RESTful 架构风格规定,数据的元操作,即 CRUD(create, read, update 和 delete, 即数据的增删查改) 操作,分别对应于HTTP方法:GET用来获取资源,POST用来新建资源(也可以用于更新资源),PUT用来更新资源,DELETE用来删除资源,这样就统一了数据操作的接口,仅通过 HTTP 方法,就可以完成对数据的所有增删查改工作。

首先 REST 前面是应该有个资源,REST 是 基于HTTP的设计架构,HTTP 的协议使用了统一资源位置(URL),也因此 RESTful 的接口应该设计成面向资源,甚至可以说,HTTP 的也应该如此。我们设计时,首先要做的就是定义一个 URL,即向外部人表达这是什么资源。但我们一般都只称呼为“接口”,这其实也是导致大家使用难以扭转思维的原因之一。比如登录接口、注销接口、下订单、取消订单等等接口这种。

要让一个资源可以被识别,需要有个唯一标识,在Web中这个唯一标识就是 URI(Uniform Resource Identifier)。 URI 既可以看成是资源的地址,也可以看成是资源的名称。如果某些信息没有使用 URI 来表示,那它就不能算是一个资源, 只能算是资源的一些信息而已。URI 的设计应该遵循可寻址性原则,具有自描述性,需要在形式上给人以直觉上的关联。这里以 github 网站为例,给出一些还算不错的URI:

https://github.com/git
https://github.com/git/git
https://github.com/git/git/blob/master/block-sha1/sha1.h
https://github.com/git/git/commit/e3af72cdafab5993d18fae056f87e1d675913d08
https://github.com/git/git/pulls
https://github.com/git/git/pulls?state=closed
https://github.com/git/git/compare/master…next

用一个 URI(统一资源定位符)指向资源,即每个 URI 都对应一个特定的资源。要获取这个资源,访问它的 URI 就可以,因此 URI 就成了每一个资源的地址或识别符。

RESTful 架构风格的服务是围绕资源展开的,是典型的 ROA 架构(面向资源的架构)

GET `http://api.config.net.cn/v1/books`      : 获取所有书籍
GET `http://api.config.net.cn/v1/books?page=2&page_size=10`      : 获取每页10条第二页中的书籍
GET `http://api.config.net.cn/v1/books/ID`    :获取指定Id的书
GET `http://api.config.net.cn/v1/orders/2021/06/28`     :2021-6-28日的订单
POST `http://api.config.net.cn/v1/orders`    : 创建一个订单
PUT `http://api.config.net.cn/v1/books`    : 更新一个书籍
DELETE  `http://api.config.net.cn/v1/orders/20210628`  :删除一个订单

规范的 API 应该包含版本信息,在 RESTful API 中,最简单的包含版本的方法是将版本信息放到 url 中,如:

  • /api/v1/posts/

  • /api/v1/drafts/

  • /api/v2/posts/

  • /api/v2/drafts/

或者是时间。另一种优雅的做法是,使用 HTTP header 中的 accept 来传递版本信息,这也是 GitHub API 采取的策略。

当然,不需要迭代版本更新则不需要添加。

注意

  1. 当参数非常多的时候,不建议使用参数路径方式;
  2. 如果参数名非常敏感,建议使用参数路径方式,可以隐藏参数名。

URI 设计技巧

使用_-来让URI可读性更好

曾经 Web 上的 URI 都是冰冷的数字或者无意义的字符串,但现在越来越多的网站使用 _- 来分隔一些单词,让URI看上去更为人性化。 例如国内比较出名的开源中国社区,它上面的新闻地址就采用这种风格, 如:http://www.oschina.net/news/38119/oschina-translate-reward-plan

建议使用 - 来隔断单词,即:

# Good
/api/featured-post/

# Bad
/api/featured_post/

使用/来表示资源的层级关系

例如上述 /git/git/commit/e3af72cdafab5993d18fae056f87e1d675913d08 就表示了一个多级的资源, 指的是 git 用户的 git 项目的某次提交记录,又例如 /orders/2012/10 可以用来表示 2012年10月 的订单记录。

# 学校中所有的男生
http://api.user.com/schools/grades/classes/boys

#检索`id`为`3248234`的学生学习的所有课程的清单。
http://api.college.com/students/3248234/courses

使用?用来过滤资源

很多人只是把?简单的当做是参数的传递,很容易造成 URI 过于复杂、难以理解。可以把?用于对资源的过滤, 例如/git/git/pulls用来表示:git项目的所有推入请求,而/pulls?state=closed用来表示:git项目中已经关闭的推入请求, 这种URL通常对应的是一些特定条件的查询结果或算法运算结果。

对于资源集合,可以通过 url 参数对资源进行过滤,如:

/api/articles?author=gevin

在获取资源的时候,有可能需要获取某些“过滤”后的资源,例如指定前10行数据

http://api.user.com/schools/grades/classes/boys?page=1&page-size=10

分页就是一种最典型的资源过滤。

,; 可以用来表示同级资源的关系

有时候我们需要表示同级资源的关系时,可以使用,;来进行分割。例如哪天 github 可以比较某个文件在随意两次提交记录之间的差异,或许可以使用/git/git/block-sha1/sha1.h/compare/e3af72cdafab5993d18fae056f87e1d675913d08;bd63e61bdf38e872d5215c07b264dcc16e4febca作为 URI。 不过,现在 github 是使用来做这个事情的,例如/git/git/compare/master…next

嵌套资源:

如果说,我们的部件有很多用户使用,URL的结构又将会是怎样的呢?

列出所有用户

GET /widgets/123/users

新增一个用户

POST /widgets/123/users

Data:
name = Andrew

嵌套资源在URL里是完全兼容的,但是超过两层嵌套就不是很好的方法了。其实这根本不需要,因为你完全可以以ID的形式参考到那些嵌套资源,总比嵌套在父类中好。例如:

/widgets/123/users/456/sports/789

这可以替换为:

/users/456/sports/789

甚至可以替换成这样:

/sports/789

状态转移到这里已经很好理解了, “会话”状态不是作为资源状态保存在服务端的,而是被客户端作为应用状态进行跟踪的。客户端应用状态在服务端提供的超媒体的指引下发生变迁。服务端通过超媒体告诉客户端当前状态有哪些后续状态可以进入。 这些类似“下一页”之类的链接起的就是这种推进状态的作用——指引你如何从当前状态进入下一个可能的状态。

通过 URI 理解,例如 /api/teams/123/members/456,你要获取某个 teams 中的某个 member,这种以定位某个资源的理解方式,去获取某个团队中的某个成员的信息。而不是直接以动作 /api/getTeamMember?team=123&member=456 这种执行某种动作的方式,去获取数据资源。

我们的核心还是满足业务功能,而不是追求 REST 的理论结果。

一些规范

  • 规则1:URI结尾不应包含/。(因为某些框架对末尾带有 / 的 URI 默认会清除掉末尾的左斜杠)

  • 规则2:正斜杠分隔符(/)必须用来指示层级关系

  • 规则3:应使用连字符( - )来提高URI的可读性

  • 规则4:不得在URI中使用下划线(_

  • 规则5:URI 路径中全都使用小写字母

  • 为了保证url格式的一致性,建议使用复数形式。

REST 指的是一组架构约束条件和原则。如果一个架构符合 REST 的约束条件和原则,我们就称它为 RESTful架构。

对于rest api资源的操作,由HTTP动词表示:

  • GET: 获取资源
  • POST: 新建资源
  • PUT:在服务器更新资源(向客户端提供改变后的所有资源)
  • PATCH: 在服务器更新资源(向客户端提供改变的属性)(一般用PUT替代了)
  • DELETE:删除资源

PATCH一般不用,用PUT

相关注解

注解 作用
@RestController @Controller + @ResponseBody组成(返回 JSON 数据格式)
@PathVariable URL 中的 {xxx} 占位符可以通过@PathVariable("xxx") 绑定到控制器处理方法的形参中
@RequestMapping 注解用于请求地址的解析,是最常用的一种注解
@GetMapping 查询请求
@PostMapping 添加请求
@PutMapping 更新请求
@DeleteMapping 删除请求
@RequestParam 将请求参数绑定到你控制器的方法参数上(是 springmvc 中接收普通参数的注解)

@RequestParam语法

语法:

@RequestParam(value="参数名",required="true/false",defaultValue="")
  • value:参数名

  • required:是否包含该参数,默认为true,表示该请求路径中必须包含该参数,如果不包含就报错。

  • defaultValue:默认参数值,如果设置了该值,required=true将失效,自动为false,如果没有传该参数,就使用默认值

API 请求的方法

实际上,在 HTTP 请求中,我们不只有 GETPOST 可用,在 REST 架构中,有以下几个重要的请求方法:GETPOSTPUTDELETE
顾名思义

类型 描述
【GET】 用于对某一(些)资源的‘获取’
【POST】 用于对某一(些)资源进行‘创建’操作
【DELETE】 用于对某一(些)资源进行‘删除 ’
【PUT】 用于对某一(些)资源进行‘更新’

格式示例

标准的格式是

http(s): //server.com /app-name /{version} /{domain} /{rest-convention}>

  • {version} 代表 api 的版本信息。
  • {domain} 代表域名 (例如:localhost / 127.0.0.1)
  • {rest-convention} 代表这个域 (domain) 下,你所访问的资源路径(约定的 rest 接口)

具体示例如下:

  • 单资源( singular-resourceX )

    url样例:/order (order即指那个单独的资源X)

    GET – 返回一个新的 order

    POST- 创建一个新的order,从 post 请求携带的内容获取值。

  • 单资源带id(singular-resourceX/{id} )

    URL样例:/order/1 ( order 即指那个单独的资源X )

    GET – 返回id1的order

    DELETE – 删除id1的order

    PUT – 更新id1的order,order的值从请求的内容体中获取。

  • 复数资源(plural-resourceX/)

    URL样例: /orders

    GET – 返回所有 orders

  • 复数资源查找(plural-resourceX/search)

    URL样例:/orders/search?name=123

    GET – 返回所有满足查询条件的order资源。(实例查询,无关联) – order名字等于123的。

  • 复数资源查找(plural-resourceX/searchByXXX)

    URL样例:/orders/searchByItems?name=ipad

    GET – 将返回所有满足自定义查询的orders – 获取所有与items名字是ipad相关联的orders。

  • 单数资源(singular-resourceX/{id}/pluralY)

    URL样例:/order/1/items (这里order即为资源X,items是复数资源Y)

    GET – 将返回所有与order id1关联的items。

  • singular-resourceX/{id}/singular-resourceY/

    URL样例:/order/1/item

    GET – 返回一个瞬时的新的与 orderid1 的关联的 item 实例。

    POST – 创建一个与order id1关联的item实例。Item的值从post请求体中获取。

  • singular-resourceX/{id}/singular-resourceY/{id}/singular-resourceZ/

    URL样例:/order/1/item/2/package

    GET – 返回一个瞬时的新的与item2和order1关联的package实例。

    POST – 创建一个新的与 item 2 和 order 1 关联的package实例,package 的值从 post 请求体中获得。

总结几个关键点,来更清晰的表述规则。

  • 在使用复数资源的时候,返回的是最后一个复数资源使用的实例。
  • 在使用单个资源的时候,返回的是最后一个单数资源使用的实例。
  • 查询的时候,返回的是最后一个复数实体使用的实例(们)。

GET,DELETE,PUT和POST的典型用法

RESTful架构应该遵循统一接口原则,统一接口包含了一组受限的预定义的操作,不论什么样的资源,都是通过使用相同的接口进行资源的访问。接口应该使用标准的 HTTP 方法如 GETPUTPOST,并遵循这些方法的语义。

如果按照 HTTP 方法的语义来暴露资源,那么接口将会拥有安全性和幂等性的特性,例如 GETHEAD 请求都是安全的, 无论请求多少次,都不会改变服务器状态。而 GETHEADPUTDELETE 请求都是幂等的,无论对资源操作多少次, 结果总是一样的,后面的请求并不会产生比第一次更多的影响。

GET

  • 安全且幂等
  • 获取表示
  • 变更时获取表示(缓存)

  • 200(OK) – 表示已在响应中发出

  • 204(无内容) – 资源有空表示

  • 301(Moved Permanently) – 资源的URI已被更新
  • 303(See Other) – 其他(如,负载均衡)
  • 304(not modified)- 资源未更改(缓存)
  • 400 (bad request)- 指代坏请求(如,参数错误)
  • 404 (not found)- 资源不存在
  • 406 (not acceptable)- 服务端不支持所需表示
  • 500 (internal server error)- 通用错误响应
  • 503 (Service Unavailable)- 服务端当前无法处理请求

POST

  • 不安全且不幂等
  • 使用服务端管理的(自动产生)的实例号创建资源
  • 创建子资源
  • 部分更新资源
  • 如果没有被修改,则不过更新资源(乐观锁)

  • 200(OK)- 如果现有资源已被更改

  • 201(created)- 如果新资源被创建

  • 202(accepted)- 已接受处理请求但尚未完成(异步处理)
  • 301(Moved Permanently)- 资源的URI被更新
  • 303(See Other)- 其他(如,负载均衡)
  • 400(bad request)- 指代坏请求
  • 404 (not found)- 资源不存在
  • 406 (not acceptable)- 服务端不支持所需表示
  • 409 (conflict)- 通用冲突
  • 412 (Precondition Failed)- 前置条件失败(如执行条件更新时的冲突)
  • 415 (unsupported media type)- 接受到的表示不受支持
  • 500 (internal server error)- 通用错误响应
  • 503 (Service Unavailable)- 服务当前无法处理请求

PUT

  • 不安全但幂等
  • 用客户端管理的实例号创建一个资源
  • 通过替换的方式更新资源
  • 如果未被修改,则更新资源(乐观锁)

  • 200 (OK)- 如果已存在资源被更改

  • 201 (created)- 如果新资源被创建

  • 301(Moved Permanently)- 资源的URI已更改
  • 303 (See Other)- 其他(如,负载均衡)
  • 400 (bad request)- 指代坏请求
  • 404 (not found)- 资源不存在
  • 406 (not acceptable)- 服务端不支持所需表示
  • 409 (conflict)- 通用冲突
  • 412 (Precondition Failed)- 前置条件失败(如执行条件更新时的冲突)
  • 415 (unsupported media type)- 接受到的表示不受支持
  • 500 (internal server error)- 通用错误响应
  • 503 (Service Unavailable)- 服务当前无法处理请求

DELETE

  • 不安全但幂等
  • 删除资源

  • 200 (OK)- 资源已被删除

  • 301 (Moved Permanently)- 资源的URI已更改

  • 303 (See Other)- 其他,如负载均衡
  • 400 (bad request)- 指代坏请求
  • 404 (not found)- 资源不存在
  • 409 (conflict)- 通用冲突
  • 500 (internal server error)- 通用错误响应
  • 503 (Service Unavailable)- 服务端当前无法处理请求

GET操作是安全的。所谓安全是指不管进行多少次操作,资源的状态都不会改变。比如我用GET浏览文章,不管浏览多少次,那篇文章还在那,没有变化。当然,你可能说每浏览一次文章,文章的浏览数就加一,这不也改变了资源的状态么?这并不矛盾,因为这个改变不是GET操作引起的,而是用户自己设定的服务端逻辑造成的。

PUTDELETE操作是幂等的。所谓幂等是指不管进行多少次操作,结果都一样。比如我用PUT修改一篇文章,然后在做同样的操作,每次操作后的结果并没有不同,DELETE也是一样。顺便说一句,因为GET操作是安全的,所以它自然也是幂等的。

POST操作既不是安全的,也不是幂等的。比如常见的POST重复加载问题:当我们多次发出同样的POST请求后,其结果是创建出了若干的资源。

HTTP中的GETPOSTPUTDELETE就对应着对这个资源的查,增,改,删 4个操作。

幂等的意味着对同一URL的多个请求应该返回同样的结果。

参考

发表评论