一句话解释就是“通过路径知晓访问资源是何, 通过请求方式知道要做什么操作”。REST全称是表述性状态转移(Representational State Transfer),那究竟指的是什么的表述? 其实指的就是资源。任何事物,只要有被引用到的必要,它就是一个资源。资源可以是实体(例如手机号码),也可以只是一个抽象概念(例如价值) 。
大致遵循以下两个规则:
- 请求 API 的 URL 表示用来定位资源;
-
请求的 METHOD 表示对这个资源进行的操作;
REST提倡所有的接口都是基于资源的,所有的增删改查操作都是对于资源状态的改变
API的URL
通过URL用来定位资源,跟要进行的操作区分开,这就意味着URL不该有任何动词
- 下面示例中的 get、create、search 等动词,都不应该出现在 REST 架构的后端接口路径中。比如:
/api/getUser/api/createApp/api/searchResult/api/deleteAllUsers
- 当我们需要对单个用户进行操作时,根据操作的方式不同可能需要下面的这些接口:
/api/getUser(用来获取某个用户的信息,还需要以参数方式传入用户 id 信息)/api/updateUser(用来更新用户信息)/api/deleteUser(用来删除单个用户)/api/resetUser(重置用户的信息)
- 可能在更新用户不同信息时,提供不同的接口,比如:
/api/updateUserName/api/updateUserEmail/api/updateUser
(以上是有状态的 URI)
以上三种情况的弊端在于:首先加上了动词,肯定是使 URL 更长了;其次对一个资源实体进行不同的操作就是一个不同的 URL,造成 URL 过多难以管理。
- 在 RESTful 风格的 URI 中则是
- URL 中不应该出现任何表示操作的动词,链接只用于对应资源;
-
URL 中应该单复数区分,推荐的实践是永远只用复数;比如 GET
/api/users
表示获取用户的列表;如果获取单个资源,传入 ID,比如/api/users/123
表示获取单个用户的信息;
按照资源的逻辑层级,对 URL 进行嵌套,比如一个用户属于某个团队,而这个团队也是众多团队之一;那么获取这个用户的接口可能是这样:GET /api/teams/123/members/234
表示获取 id
为 123
的小组下,id
为 234
的成员信息。
按照类似的规则,可以写出如下的接口
/api/teams
(对应团队列表)-
/api/teams/123
(对应ID
为123
的团队) -
/api/teams/123/members
(对应ID
为123
的团队下的成员列表) -
/api/teams/123/members/456
(对应ID
为123
的团队下ID
为456
的成员)
(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 采取的策略。
当然,不需要迭代版本更新则不需要添加。
注意
- 当参数非常多的时候,不建议使用参数路径方式;
- 如果参数名非常敏感,建议使用参数路径方式,可以隐藏参数名。
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 请求中,我们不只有 GET
和 POST
可用,在 REST 架构中,有以下几个重要的请求方法:GET
,POST
,PUT
,DELETE
。
顾名思义
类型 | 描述 |
---|---|
【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 – 返回
id
是1
的orderDELETE – 删除
id
是1
的orderPUT – 更新
id
是1
的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
id
是1
关联的items。 -
singular-resourceX/{id}/singular-resourceY/
URL样例:
/order/1/item
GET – 返回一个瞬时的新的与
order
的id
为1
的关联的item
实例。POST – 创建一个与order
id
是1
关联的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 方法如 GET
,PUT
和 POST
,并遵循这些方法的语义。
如果按照 HTTP 方法的语义来暴露资源,那么接口将会拥有安全性和幂等性的特性,例如 GET
和 HEAD
请求都是安全的, 无论请求多少次,都不会改变服务器状态。而 GET
、HEAD
、PUT
和 DELETE
请求都是幂等的,无论对资源操作多少次, 结果总是一样的,后面的请求并不会产生比第一次更多的影响。
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
操作引起的,而是用户自己设定的服务端逻辑造成的。
PUT
,DELETE
操作是幂等的。所谓幂等是指不管进行多少次操作,结果都一样。比如我用PUT
修改一篇文章,然后在做同样的操作,每次操作后的结果并没有不同,DELETE
也是一样。顺便说一句,因为GET
操作是安全的,所以它自然也是幂等的。
POST
操作既不是安全的,也不是幂等的。比如常见的POST
重复加载问题:当我们多次发出同样的POST
请求后,其结果是创建出了若干的资源。
HTTP中的GET
,POST
,PUT
,DELETE
就对应着对这个资源的查,增,改,删 4个操作。
幂等的意味着对同一URL的多个请求应该返回同样的结果。