RESTful最佳实践
哲学
- 不要为了RESTful而RESTful
- 在能表达清楚的情况下,简单就是美
接口路径设计
接口设计原则
URI指向的是唯一的资源对象
示例: 指向ID为yanbo.ai的Account对象
GET http://~/$version/accounts/yanbo.ai |
URI可以隐式指向唯一的集合列表
示例: 隐式地指向trades list 集合
GET http://~/$version/trades/(list) |
聚合资源必须通过父级资源操作
示例: Profile是User的聚合资源,User有一个唯一且私有的Profile资源,只能通过User操作Profile。
更新user_id为123456的Profile资源 |
组合资源要避免资源路径嵌套
示例: 一个系统里面包含多个 applications,一个 application 又包含多个 users。那获取 user 资源的路径应该是怎样的?
看一个路径嵌套的例子:
GET http://~/$version/systems/:systemId/applications/:applicationId/users/:userId |
这样做是不合理的,它会让你的接口变得越来越混乱和缺少灵活性。正确的做法是:
GET http://~/$version/systems/:systemId |
Http Methods
| HTTP Operation | Description |
|---|---|
| GET | 获取,查找 |
| POST | 新增创建 |
| PUT | 更新 |
| PATCH | 部分更新 |
| DELETE | 删除 |
URL组成
- 网络协议(HTTP, HTTPS)
- 服务器地址
- 版本
- 接口名称
- ?参数列表
GET https://github.com/v1/trades |
为什么需要版本?
当服务被更多其他系统使用的时候,服务的可用性和上下兼容变得至关重要。被外部系统依赖的服务在升级时是一个非常麻烦的事情,既要发布新的接口,又要保留旧的接口留出时间让调用者去升级。在URL中加入Version标示能很好地解决上下兼容(新老版本共存)问题。
示例1: URL中新增了Path parameter
v1版本
GET http://~/v1/trades?user_id=123456 |
v2版本
GET http://~/v2/:user_id/trades |
示例1中的user_id参数在v2版本被加入到path parameter中,使用$version保证了v1和v2接口的共存。
示例2: 数据接口发生变化
v1版本
GET http://~/v1/accounts/yanbo.ai |
v2版本
GET http://~/v2/accounts/yanbo.ai |
示例2中的接口返回数据结构已经发生了变化。使用$version保证了v1和v2接口的共存。
URL定义限制
- 不使用大写字母
- 使用中线
-代替下划线_ - 参数列表应该被encode过
接口分类
资源对象的CURD操作
GET http://~/$version/trades 获取trades列表 |
服务型接口
使用services标识,根据服务的属性选择http方法。
http://~/services/$version/server-name |
系统设置
使用settings标识,根据服务的属性选择http方法。
http://~/settings/$version/server-name |
示例1: 搜索
GET http://~/services/$version/search?q=filter?category=file |
示例2: 任务队列操作
PUT http://~/services/$version/queued/jobs 往任务队列里面添加一个新的任务 |
示例3: 更改界面语言环境
PUT http://~/settings/$version/gui/lang |
为什么需要区分?
Microservices是一个全新的概念,它主要的观点是将一个大型的服务系统分解成多个微型系统。每个微型系统都能独立工作,并且提供各种不同的服务。独立运行的特点使微型系统之间不会产生相互影响,其中的一个微型系统宕机并不会牵连到其他的微型系统。这种架构使分布式系统的节点数量大大提升。因为RESTful服务是无状态的,所以这种分解并不会带来状态共享的问题。
2.路由规则(逻辑)
当我们需要对不同属性的接口做路由规则的时候,按功能划分接口是一个很好的方案。例如:我们要对系统设置接口设置增加更严格的调用限制。
缓存
网络接口相对于堆栈接口来说数据传输极其不稳定,尽可能地减少数据传输不仅能控制这种风险还能减少流量。使用缓存还能有效地提高后台的吞吐量。
后台在响应请求时使用响应头E-Tag或Last-Modified来标记数据的版本,前台在发送请求时将数据版本通过请求头If-Match帮助后台判断缓存的使用。
Request Header
If-Match: 2390239059405940 |
Response Header
E-Tag: 2390239059405940 |
Bookmarker
在实际的环境中,有大量的查询需求是相同的。将这些搜索需求标签化能降低使用难度也可以达到重用的目的。
示例1: 查找状态为关闭的订单
普通方式
GET http://~/$version/trades?status=closed&sorting=-created_at |
Bookmarker
GET http://~/$version/trades#recently_closed |
或
GET http://~/$version/trades/recently_closed |
HATEOAS
HATEOAS通过Web Linking的方式来描述程序的状态信息
Link 主要包含以下属性:
| Property | Description |
|---|---|
| rel | 关联内容 |
| href | URL |
| type | 媒体类型 |
| method | Http Method |
| title | 标题 |
| arguments | 参数列表 |
| value | 返回值 |
Rel 可能为以下值:
| Value | Description |
|---|---|
| next | 下一步 |
| prev | 上一步 |
| first | 第一步,最前 |
| last | 最后一步,最后 |
| source | 来源 |
| self | 资源自身,相对于this |
Web Linking 可以通过两种方式传递至客户端:
Http Header
Link: <http://~/$version/trades?page_no=10>; rel="next", <http://~/$version/trades?page_no=19>; rel="last" |
Http JSON Body
{
|
示例1: 用户注册业务
- 用户填写E-Mail与密码
- 完善用户资料
Register Request
POST http://~/$version/accounts |
Register Response
Headers: |
Profile Request
POST http://~/$version/accounts/yanbo.ai/profiles |
Profile Response
Headers: |
示例2: 请看下节<分页>
HATEOAS在解决什么问题?
HATEOAS是Hypermedia as the Engine of Application State的缩写形式,中文意思为:超媒体应用状态引擎。它的核心思想是使用超媒体表达应用状态,与hypertext-driven思想是一致的。在此之前,我们大多数的程序业务控制在前台完成。例如:我们会在前台做注册流程,我们在前台判定下一步应该做什么,可以做什么。当使用HATEOAS时,这些状态流程控制都在应用程序的后台完成。我们使用超媒体来表达前台做完某一步骤之后可以做哪些? 这样一来,前台的任务就变得相当简单了,前台需要处理的是理解状态表述,数据收集和结果显示。
思考
HATEOAS会带来怎样的改变? 使用它的意义在哪?
分页
Request
GET http://~/$version/trades?page=10&pre_page=100 |
Response
Link Header
Link: <http://~/$version/trades?page=11&pre_page=100>; rel="next", <http://~/$version/trades?page=19&pre_page=100>; rel="last" |
JSON Body
{
|
安全
调用限制
为保证服务的可用性应对服务进行调用过载保护
Response Headers
X-RateLimit-Limit: 3000 调用量的最大限制 |
安全验证
RESTful服务使用Oauth2的方式进行调用授权,使用http请求头Authorization设置授权码; 必须使用User-Agent设置客户端信息, 无User-Agent请求头的请求应该被拒绝访问。
Request Header
User-Agent: Data-Server-Client |
为什么建议使用Oauth2授权?
Oauth2的参与者为:客户端,资源所有者,授权服务器,资源服务器。客户端先从资源所有者得到授权码之后使用授权码从授权服务器得到token,再使用token调用资源服务器获取经过资源所有者授权使用的资源。这种授权方式的特点有:
资源所有者可以随时撤销授权许可
可以通过撤销
token拒绝客户端的调用资源服务器可以拒绝客户端的调用
通过这三种方式可以做到对资源的严格保护。资源的访问权限也把握在资源所有者的手中,而不是资源服务器。
当然,Oauth2授权框架也允许受信任的客户端直接使用token调用资源服务器获取资源。这种灵活性完全取决于客户端类型和对资源的保护程度。
为什么授权码要放在Http Header中?
- WEB服务器对访问做记录已经成为了行业的一个标准,访问记录不仅可以用来做访问量统计还能用来做访问特征分析。互联网广告平台就是利用访问记录来做精准营销的。如果
token(授权码)包含在URL中就有很大的安全风险。 - 包含在URL中的
token串可能被进行重定向传递。通过这两种方式入侵者可以不通过授权而使用泄漏的授权码访问那些受保护的数据,会造成数据泄漏的风险。
以Tomcat为例,访问日志为:
127.0.0.1 - - [24/Jun/2014:14:38:04 +0800] "GET /v1/accounts/yanbo.ai?token=dgdreLJLJLER798989erJKJK HTTPS/1.1" 200 343 |
通过对访问日志的提取,很容易得到token信息。
数据设计
交互原则
- 查询,过滤条件使用query string。
- 用来描述数据或者请求的元数据放Header中,例如
X-Result-Fields。 - Content body 仅仅用来传输数据。
- 数据要做到拿来就可用的原则,不需要“拆箱”的过程。
- 使用ISO-8601格式表达时间字段,例如:
2014-04-05T14:30Z。
结构
使用JSON格式传输数据,在http请求头和响应头申明Content-Type。返回的数据结构应该做到尽可能简单,不要过于包装。响应状态应该包含在响应头中!
Request
Accept: application/json |
Response
Content-Type: application/json;charset=UTF-8 |
错误的做法
{
|
正确的做法
Response Headers: |
示例1: 创建User对象
POST http://~/$version/users |
为什么是JSON?
JSON 是一种可以跨平台高扩展的轻量级的数据交换格式。易于人阅读和编写,同时也易于机器解析和生成。
属性定义限制
- 不能使用大写(大小写友好)
- 使用下划线_命名(连接两个单词)
- 属性和字符串值必须使用双引号””
提取部分字段
无状态服务器应该允许客户端对数据按需提取。在请求头使用X-Result-Fields指定数据返回的字段集合。
例如:trade 有trade_id, trade_name, created_at 三个属性,客户端只需其中的trade_id与trade_name属性。
Request Header
X-Result-Fields: trade_id,trade_name |
子对象描述
数据里面的子对象使用URI描述不应该被提取,除非用户指定需要提取子对象
示例: trade里面的order对象
错误的做法
{
|
正确的做法
{
|
应用指定提取子对象,需要在请求头声明X-Expansion-Fields
Request
X-Expansion-Fields: true |
为什么要客户端指定提取子对象时才提取?
懒模式服务能够最大程度地节省运算资源。虽然与客户端交互的次数有所增加,但是能做到按需提取,按需响应,这也是响应式设计的一大特点。客户端的用户行为模式无法真实地模拟,也就无法确定哪些资源需要做到一次性推送,让客户端按需使用是一个不错的方式。
关于空字段
应该在返回结果里面剔除空字段,因为null值传输到客户端并没有实际的含义,反而增加了占用空间。
Tips
使用HTTP Header时,优先使用合适的标准头属性。用X-作为前缀自定义一个头属性,例如: X-Result-Fields。
状态码&错误处理
应用状态码
| Code | HTTP Operation Body | Contents | Description |
|---|---|---|---|
| 200 | GET,PUT | 资源 | 操作成功 |
| 201 | POST | 资源,元数据 | 对象创建成功 |
| 202 | POST,PUT,DELETE,PATCH | N/A | 请求已经被接受 |
| 204 | DELETE,PUT,PATCH | N/A | 操作已经执行成功,但是没有返回数据 |
| 301 | GET | link | 资源已被移除 |
| 303 | GET | link | 重定向 |
| 304 | GET | N/A | 资源没有被修改 |
| 400 | GET,PSOT,PUT,DELETE,PATCH | 错误提示(消息) | 参数列表错误(缺少,格式不匹配) |
| 401 | GET,PSOT,PUT,DELETE,PATCH | 错误提示(消息) | 未授权 |
| 403 | GET,PSOT,PUT,DELETE,PATCH | 错误提示(消息) | 访问受限,授权过期 |
| 404 | GET,PSOT,PUT,DELETE,PATCH | 错误提示(消息) | 资源,服务未找到 |
| 405 | GET,PSOT,PUT,DELETE,PATCH | 错误提示(消息) | 不允许的http方法 |
| 409 | GET,PSOT,PUT,DELETE,PATCH | 错误提示(消息) | 资源冲突,或者资源被锁定 |
| 415 | GET,PSOT,PUT,DELETE,PATCH | 错误提示(消息) | 不支持的数据(媒体)类型 |
| 429 | GET,PSOT,PUT,DELETE,PATCH | 错误提示(消息) | 请求过多被限制 |
| 500 | GET,PSOT,PUT,DELETE,PATCH | 错误提示(消息) | 系统内部错误 |
| 501 | GET,PSOT,PUT,DELETE,PATCH | 错误提示(消息) | 接口未实现 |
容器状态码
容器状态码是指http容器的状态码,应用不应该使用或限制使用
| Code | HTTP Operation | Body Contents | Description |
|---|---|---|---|
| 303 | GET | link | 静态资源被移除,应用限制使用 |
| 503 | GET,PSOT,PUT,DELETE,PATCH | text body | 服务器宕机 |
Tips
4开头的错误用来表达来自于客户端的错误,例如: 未授权,参数缺失。5开头的错误用来表达服务端的错误,例如: 在连接外部系统(DB)发生的IO错误。
错误信息格式
错误信息应该包含下列内容:
- 错误标题
message, 必须 - 错误代码
error code, 必须 - 错误信息
error message, 必须 - 资源
resource, 可选 - 属性
field, 可选 - 文档地址
document, 可选
Tips
Error Code 尽可能做到简洁明了,提取异常的关键字并且使用下划线_把它们连接起来。
示例: 调用频率超过限制,Response:
Headers: |
锦上添花
- 格式化(Pettyprint)JSON数据(返回结果)并且使用gzip压缩,Pettyprint易于阅读,多余的空格在经过gzip压缩之后占用空间比压缩之前更小。
- 重写
Server头 - 返回
X-Powered-By
Response Headers
X-Pretty-Print: true |
附页
框架&工具
- http://raml.org/
- https://jersey.java.net/
- https://dropwizard.github.io/dropwizard/
- https://github.com/aiyanbo/restful-hub/
参考资料
- http://restfulobjects.org/
- https://developer.github.com/v3/
- http://tools.ietf.org/html/rfc5988
- http://www.vinaysahni.com/best-practices-for-a-pragmatic-restful-api
- https://developer.yahoo.com/social/rest_api_guide/http-response-codes.html
- http://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm
- http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven
RESTful最佳实践的更多相关文章
- 前后端分离-Restful最佳实践
前后端分离-Restful最佳实践 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任.
- RESTful最佳实践之基于 jersey 的增删改查
jersey-rest-demo 增删改查 项目地址:https://github.com/CoderDream/jersey-rest-demo 源代码:http://download.csdn.n ...
- 【Restful】三分钟彻底了解Restful最佳实践
REST是英文representational state transfer(表象性状态转变)或者表述性状态转移;Rest是web服务的一种架构风格;使用HTTP,URI,XML,JSON,HTML等 ...
- RESTful API 设计最佳实践
背景 目前互联网上充斥着大量的关于RESTful API(为了方便,以后API和RESTful API 一个意思)如何设计的文章,然而却没有一个"万能"的设计标准:如何鉴权?API ...
- [转]10个有关RESTful API良好设计的最佳实践
Web API已经在最近几年变成重要的话题,一个干净的API设计对于后端系统是非常重要的. 通常我们为Web API使用RESTful设计,REST概念分离了API结构和逻辑资源,通过Http方法GE ...
- RESTful接口设计原则/最佳实践(学习笔记)
RESTful接口设计原则/最佳实践(学习笔记) 原文地址:http://www.vinaysahni.com/best-practices-for-a-pragmatic-restful-api 1 ...
- 10个有关RESTful API良好设计的最佳实践
Web API已经在最近几年变成重要的话题,一个干净的API设计对于后端系统是非常重要的. 通常我们为Web API使用RESTful设计,REST概念分离了API结构和逻辑资源,通过Http方法GE ...
- ****RESTful API 设计最佳实践(APP后端API设计参考典范)
http://blog.jobbole.com/41233/ 背景 目前互联网上充斥着大量的关于RESTful API(为方便,下文中“RESTful API ”简写为“API”)如何设计的文章,然而 ...
- RESTful API 设计最佳实践(转)
摘要:目前互联网上充斥着大量的关于RESTful API(为了方便,以后API和RESTful API 一个意思)如何设计的文章,然而却没有一个”万能“的设计标准:如何鉴权?API格式如何?你的API ...
随机推荐
- NLPIR
# coding: utf-8 import pynlpir from pynlpir import nlpir nlpir.Init(nlpir.PACKAGE_DIR, nlpir.UTF8_CO ...
- python 字符串,数学之间的不可描述的关系
首先说一下输入: >>> a=raw_input(" ") 1.234 >>> a '1.234' >>> 可以看到使用raw ...
- [2018湖南省队集训] 6.24 T1 marshland
题面在这里! 一开始感觉像一个类似二分图的最小割,于是成功跑偏2333333 很容易发现一个关键性质,'L'的两个角落在的偶数格 的行(或者列)的奇偶性一定不同.... 于是我们再把偶数格按照行(或者 ...
- 【图论】The Bottom of a Graph
[POJ2553]The Bottom of a Graph Time Limit: 3000MS Memory Limit: 65536K Total Submissions: 11182 ...
- 【线段树】Atlantis
Atlantis Time Limit: 1000MS Memory Limit: 10000K Total Submissions: 23181 Accepted: 8644 Descrip ...
- POJ 3378 Crazy Thairs(树状数组+DP)
[题目链接] http://poj.org/problem?id=3378 [题目大意] 给出一个序列,求序列中长度等于5的LIS数量. [题解] 我们发现对于每个数长度为k的LIS有dp[k][i] ...
- 检测是否为n的因子 Exercise07_06
/** * @author 冰樱梦 * 时间:2018年下半年 * 题目:检测是否为n的因子 * */ public class Exercise07_06 { public static void ...
- Activity(活动)生命周期--系统回收活动数据存储
当一个活动进入停止状态的时候,是有可能被系统回收的.那如果处于停止状态的活动被系统回收了,而它上面却有我们所需要数据该如何保存呢?(类似于:打开qq进入下一个界面没有进去,返回的时候仍然不需要你输入账 ...
- mysql函数的使用create function myfunction(...
use test //必须在某个数据库下面创建 delimiter $ //创建 CREATE FUNCTION myFunction(ln int, ls int) //参数这里不能加def ...
- 基本C库函数
当编写驱动程序时,一般情况下不能使用C标准库的函数.Linux内核也提供了与标准库函数功能相同的一些函数,但二者还是稍有差别. 类别 函数名 功能 函数形成 参数 描述 字符串转换 simple_st ...