RESTful
RESTful
RESTFUL是一种网络应用程序的设计风格和开发方式,基于HTTP,可以使用XML格式定义或JSON格式定义。RESTFUL适用于移动互联网厂商作为业务接口的场景,实现第三方OTT调用移动网络资源的功能,动作类型为新增、变更、删除所调用资源。
[1]“RESTful_百度百科,” Baike, https://baike.baidu.com/item/RESTful/4406165(访问时间 Jan. 1, 1970).
RESTful 三要素
RESTFul主要有以下三个要点:
- 基于HTTP协议URL对外暴露
- 使用XML/JSON格式定义
- 根据不同行为使用不同的请求方式
以下从这三个方面分别阐述。
基于HTTP协议URL对外暴露
在RESTful协议中,URL被视为资源,要求使用名词进行说明。
RESTful API一般标准如下:
http(s)://域名:端口[/版本]/资源1[/子资源2/.../子资源n][/路径变量]
举例:
GET http(s)://edu.lagou.com/v1.1/blog/article/10
若需要返回数据集合,最后一级资源应使用负数。例如:
GET http(s)://edu.lagou.com/v1.1/blog/articles?categoryId=10
表示查询分类号为10的文章数据,调用接口版本为v1.1。
反例:
GET http(s)://edu.lagou.com/v1.1/blog/article/select_by_id?id=10
在该反例中,描述的是一个动作,而非一个代表资源的名词。
使用XML/JSON格式定义
接口返回必须是JSON或XML数据,绝不能包含与具体展示相关的内容。反例如下:
GET http(s)://edu.lagou.com/v1.1/blog/article/10
{
"code": "0",
"message": "success",
"data": "<h1>员工姓名:张三</h1>"
}
在这个反例中,结构返回数据包含<h1>
标签,这个标签只能由浏览器解析展现,与具体展示相关。这样做,若调用该接口的是APP,则无法使用。应修改为如下返回方式:
GET http(s)://edu.lagou.com/v1.1/blog/article/10
{
"code": "0",
"message": "success",
"data": {
"name": "张三",
"age": 30,
}
}
根据不同行为使用不同的请求方式
在RESTful语义中,不同的HTTP动作对应着不同的行为。常用的如:
- GET - SELECT: 从服务器拉取资源
- DELETE - DELETE: 从服务器删除资源
- POST - CREATE: 在服务器创建一个资源
- PUT - UPDATE: 在服务器更新资源
示例,若请求如下:
GET http(s)://edu.lagou.com/v1.1/blog/article/10
则表示:向服务器查询编号为10的员工数据。若请求为:
POST http(s)://edu.lagou.com/v1.1/blog/article/10
{
"id": 10,
"name": "张三",
"age": 25,
}
则表示,在服务器中新增一个编号为10的员工。类似的:PUT
代表更新员工数据,DELETE
代表删除员工数据。
RESTful 接口的设计规则和注意事项
接口要保证幂等性设计
幂等性:当多次重复请求时,接口应当能够保证产生于预期相符的结果。
若设计了一个为员工涨薪的接口:
PUT https://edu.lagou.com/employee/salary
{
"id": 1,
"incr_salary": 500,
}
后端伪代码可能会这样写:
// 查询员工数据
Employee employee = employeeService.selectById(1);
// 更新工资
employee.setSalary(employee.getSalary() + incrSalary);
// 执行更新语句
employeeService.update(employee);
但在分布式环境下,为了保证消息的高可靠性,客户端往往会采用重试或消息补偿的形式,重复发送同一个请求。此时,每当一次重复请求到达时,该员工的工资都会被加500,最终员工拿到的工资超过了员工的应得工资。此时,这个接口的设计就是不理想的。
为了解决这个问题,可以采用很多方法,这里浅谈一下。
- 乐观锁
在员工表上额外添加一个version字段,代表当前的数据版本。每次修改员工数据时,都要使该字段加一。同时,这个version应当被附加在请求中,如下所示:
PUT https://edu.lagou.com/employee/salary
{
"id": 1,
"incr_salary": 500,
"version": 1,
}
服务器代码也应更新:
// 查询员工数据
Employee employee = employeeService.selectById(1);
// 更新工资
employee.setSalary(employee.getSalary() + incrSalary);
// 执行成功,version变更
employee.setVersion(employee.getVersion() + 1);
// 执行更新语句
employeeService.update(employee);
此时的更新SQL语句变为:
update employee set salary = 3500, version = 2 where id = 1 and version = 1
此时,即使客户端重复发送相同请求,由于version的存在,只会产生一个版本变更。
- 幂等表
- 分布式锁
- 状态机
标准化的响应结果集
RESTful接口的响应结果应当保持全局相同的语义和结构。以行业常见的数据格式为例:
{
code: "0",
message: "success",
data: {
empoyee: {
name: "张三",
salary: 3500,
version: 2,
}
}
}
-
code: 服务器处理结果。
-
message: 返回的消息内容。
-
[data]: 响应返回的额外数据,如:查询结果、新增或者更新后的数据。
在语义层面也应保持一致。例如:服务器处理成功的code
值为0,如遇到异常,所有开发人员也应遵守统一的code
,如:
- 1xx :参数异常
- 2xx:数据库处理异常
等。
接口设计采用无状态方案
无状态方案:RESTful接口每一个请求进来,都应该能得到相同的预期。
若存在如下经典设计:通过Nginx前置做负载均衡,将用户请求打到某一个Tomcat节点上,用户数据会保存在Tomcat服务器的Session会话中。
若用户请求1打到Tomcat服务器1上,数据保存在Tomcat服务器1的Session中。请求2打到Tomcat服务器2上,则会缺少数据。此时则是有状态的。常用的去状态化方案一般有一下几种:
- 客户端存储数据
将用户数据保存在客户端的cookie或者其他介质上,这样用户数据可以被附加在请求体或请求头中,发送到服务器进行处理。
但很明显,用户数据存储被包含在网络传输过程中,若网络被抓包,用户数据可能泄露。所以必须在客户端进行加密存储,再在服务端进行解密。著名的JWT(Json Web Token),就是使用这种方案实现用户认证的去状态化。
- 后端统一存储状态数据
例如,可以在tomcat后端加入redis,进行统一存储。这样既可以保护用户敏感数据,也可以做到去状态化。但是,会导致架构复杂度增加,部署成本提高。