RESTful API 设计指南及请求返回规范
当前发展趋势,前后端逐渐分离成不同的项目,前端负责页面路由和页面渲染,后端通过API接口提供数据支持。因此,必须有一种统一的机制,方便不同的前端设备与后端进行通信。这导致API构架的流行,甚至出现"API First"的设计思想。RESTful API是目前比较成熟的一套互联网应用程序的API设计理论。
一、什么是RESTful?
1.1 域名
一般情况下,放置跨域调用,API接口放在项目本域名下:
https://example.com/api/
1.2 路径(Endpoint)
路径又称"终点"(endpoint),表示API的具体网址。
在RESTful架构中,每个网址代表一种资源(resource),所以网址中不能有动词,只能有名词,而且所用的名词往往与数据库的表格名对应。一般来说,数据库中的表都是同种记录的"集合"(collection),所以API中的名词也应该使用复数。
举例来说,有一个API提供动物园(zoo)的信息,还包括各种动物和雇员的信息,则它的路径应该设计成下面这样。
https://example.com/api/zoos
https://example.com/api/animals
https://example.com/api/employees
1.3 HTTP动词
对于资源的具体操作类型,由HTTP动词表示。
常用的HTTP动词有下面五个(括号里是对应的SQL命令)。
GET(SELECT):从服务器取出资源(一项或多项)。
POST(CREATE):在服务器新建一个资源。
PUT(UPDATE):在服务器更新资源(客户端提供改变后的完整资源)。
PATCH(UPDATE):在服务器更新资源(客户端提供改变的属性)。
DELETE(DELETE):从服务器删除资源。
还有两个不常用的HTTP动词。
HEAD:获取资源的元数据。
OPTIONS:获取信息,关于资源的哪些属性是客户端可以改变的。
下面是一些例子。
GET /zoos:列出所有动物园
POST /zoos:新建一个动物园
GET /zoos/ID:获取某个指定动物园的信息
PUT /zoos/ID:更新某个指定动物园的信息(提供该动物园的全部信息)
PATCH /zoos/ID:更新某个指定动物园的信息(提供该动物园的部分信息)
DELETE /zoos/ID:删除某个动物园
GET /zoos/ID/animals:列出某个指定动物园的所有动物
DELETE /zoos/ID/animals/ID:删除某个指定动物园的指定动物
1.4 过滤信息(Filtering)
如果记录数量很多,服务器不可能都将它们返回给用户。API应该提供参数,过滤返回结果。
下面是一些常见的参数。
?size=10:指定返回记录的数量
?page=0:指定返回记录的开始位置。
?page=2&size=100:指定第几页,以及每页的记录数。
?sort=name,asc:指定返回结果按照哪个属性排序,以及排序顺序。
?animal_type_id=1:指定筛选条件
参数的设计允许存在冗余,即允许API路径和URL参数偶尔有重复。比如,GET /zoo/ID/animals 与 GET /animals?zoo_id=ID 的含义是相同的。
1.5 状态码(Status Codes)
服务器向用户返回的状态码和提示信息,常见的有以下一些(方括号中是该状态码对应的HTTP动词)。
200 OK - [GET]:服务器成功返回用户请求的数据,该操作是幂等的(Idempotent)。
201 CREATED - [POST/PUT/PATCH]:用户新建或修改数据成功。
202 Accepted - [*]:表示一个请求已经进入后台排队(异步任务)
204 NO CONTENT - [DELETE]:用户删除数据成功。
400 INVALID REQUEST - [POST/PUT/PATCH]:用户发出的请求有错误,服务器没有进行新建或修改数据的操作,该操作是幂等的。
401 Unauthorized - [*]:表示用户没有权限(令牌、用户名、密码错误)。
403 Forbidden - [*] 表示用户得到授权(与401错误相对),但是访问是被禁止的。
404 NOT FOUND - [*]:用户发出的请求针对的是不存在的记录,服务器没有进行操作,该操作是幂等的。
406 Not Acceptable - [GET]:用户请求的格式不可得(比如用户请求JSON格式,但是只有XML格式)。
410 Gone -[GET]:用户请求的资源被永久删除,且不会再得到的。
422 Unprocesable entity - [POST/PUT/PATCH] 当创建一个对象时,发生一个验证错误。
500 INTERNAL SERVER ERROR - [*]:服务器发生错误,用户将无法判断发出的请求是否成功。
二、RESTful风格示例
2.1 RESTful风格接口设计
在Spring MVC 中,RESTful API的定义对应为Controller层。这里使用一个对用户(Users)常见接口来说明Restful风格的设计规范。前面介绍过的RESTful的接口定义规范:
GET(SELECT):从服务器取出资源(一项或多项)。
POST(CREATE):在服务器新建一个资源。
PUT(UPDATE):在服务器更新资源(客户端提供改变后的完整资源)。
PATCH(UPDATE):在服务器更新资源(客户端提供改变的属性)。
DELETE(DELETE):从服务器删除资源。
我们按照规范设计如下的接口:
@RestController
public class UserController {
@Autowired
private UserRepository userRepository;
/**
* 获取用户列表
*/
@GetMapping("/users")
@ResponseStatus(HttpStatus.OK)
public List getUsers(){
return userRepository.findUserList();
}
/**
* 创建一个用户
*/
@PostMapping(value = "/users")
@ResponseStatus(HttpStatus.CREATED)
public Object addUser(@RequestBody Users users){
return userRepository.save(users);
}
/**
* 根据用户id获取用户信息
*/
@GetMapping(value = "/users/{id}")
@ResponseStatus(HttpStatus.OK)
public Object getUser(@PathVariable("id") String id) throws NotFoundException
{
return userRepository.getUserById(id);
}
/**
* 根据用户id删除一个用户
*/
@DeleteMapping(value = "/users/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void deleteUser(@PathVariable("id") String id)
{
userRepository.deleteUser(id);
}
/**
* 根据用户id修改用户信息
*/
@PutMapping(value = "/users/{id}")
@ResponseStatus(HttpStatus.CREATED)
public Users updateUser(@PathVariable("id") String id, @RequestBody Users user)
{
return userRepository.update(id, user);
}
}
在这些接口上,使用了如下注解:
- @GetMapping
- @PostMapping
- @DeleteMapping
- @PutMapping
Spring 4.3之后推荐使用这种写法,旨在简化常用的HTTP方法的映射,并可以更好地表达被注解方法的语义。如@GetMapping实际上是一个组合注解,可以直接代替@RequestMapping(method = RequestMethod.GET),其他同理。
同时,我们在每个接口上显示得使用了注解@ResponseStatus,用来标识接口正常返回时的HTTP状态码。另外,我们还需要注意每个接口的返回结果,除了删除用户,其他每个接口都有返回值。这是因为RESTful 规范中提到:
GET /collection:返回资源对象的列表(数组)
GET /collection/resource:返回单个资源对象
POST /collection:返回新生成的资源对象
PUT /collection/resource:返回完整的资源对象
PATCH /collection/resource:返回完整的资源对象
DELETE /collection/resource:返回一个空文档
2.2 关于PUT和PATCH
根据RESTful的思想,我们知道更新操作可以分为全部更新和部分更新。结合HTTP语义,可以表示为:
PUT /users/{id}:更新某个人的信息(提供此人的全部信息)
PATCH /users/{id}:更新某个人的信息(提供此人的部分信息,比如只修该age字段)
但实际上,PATCH语义的应用并不广泛。所以,为了方便,我将两个接口合在一起,同时支持全部和部分更新,HTTP动词使用PUT,仅供参考,大家酌情而定。
三、返回信息结构
前后端分离之后,前后端开发人员依靠接口文档进行开发,这时候规范的返回参数结构就变得尤为重要,约定好返回参数将大大提升开发效率,下面对返回参数进行规范说明。
以下涉及所有参考代码,可点我查看
3.1 所有正确的返回JSON
状态码为2xx
- 集合对象返回的结构规范
{
"total": 200, // 符合条件数据集合的总条目数
"data": [{
// 符合条件当前分页的条件列表
}]
}
这里需要定义一个用于返回的值对象:
/**
* 请求返回集合的封装值对象
*/
public class ResponseCollection {
private Long total;
private Object data;
setter and getter omit...
}
改造controller层相应方法:
/**
* 获取用户列表
*/
@GetMapping("/users")
@ResponseStatus(HttpStatus.OK)
public ResponseCollection getUsers(Pageable pageable){
Page result= userRepository.findUserList(pageable);
//将结果封装到集合类结果的值对象中
ResponseCollection resp= new ResponseCollection();
resp.setTotal(result.getTotalElements());
resp.setData(result.getContent());
return resp;
}
将的数据组装后返回,结合上面的示例,对获取用户列表接口进行请求:
localhost:8080/users?page=0&size=10
返回集合对象,结构如下:
{
"total": 2,
"data": [
{
"id": "bc63312b-0536-412c-9d36-54614002d32a",
"userName": "张三",
"age": 25
},
{
"id": "ec7201d8-5c9f-4fa7-9a83-4a1f92192bc1",
"userName": "李四",
"age": 18
}
]
}
- 单个对象返回的结构规范
{
//具体对象内容
}
单个对象的查询结果无需封装直接返回即可,对根据id获取用户详情接口进行请求:
localhost:8080/users/bc63312b-0536-412c-9d36-54614002d32a
返回单个对象,结构够如下:
{
"id": "bc63312b-0536-412c-9d36-54614002d32a",
"userName": "张三",
"age": 25
}
3.2 有错误时候的返回JSON
服务器异常的结构规范:
{
"status": 500,
"error": "INTERNAL SERVER ERROR",
"message": "服务器发生错误"
}
参数错误的结构规范:
{
"status": 422,
"error": "UNPROCESABLE ENTITY",
"message": "提交的数据格式有错误",
"details": [{
"field": "email", // 有格式验证错误的属性名
"message": "请输入正确的email地址" // 详细的错误信息
}]
}
对于错误时候的返回,应当兼容http的状态码,尽量避免出现返回状态是200,而在返回内容里表明存在错误。其实spring boot提供了一种更好的解决方方案,可以使用Spring 全局异常处理机制,我们可以通过使用@ControllerAdvice注解定义全局统一的异常处理类来完成需求。也即是说,在处理错误时,我们不再直接返回一个封装的值对象,而采用异常机制。下面介绍一下实现方式。
首先定义一个处理参数异常类,该类专门处理非法参数的异常:
/**
* 自定义参数异常类
*/
public class ParamsException extends Exception{
private List
新建类RestExceptionHandler用于处理全局异常,使用注解@ControllerAdvice,如下:
/**
* 全局异常处理
*/
@ControllerAdvice
public class RestExceptionHandler {
/**
* 处理参数异常
*/
@ExceptionHandler(value = ParamsException.class)
@ResponseBody
@ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY)
public ResponseParamsError handleParamsException(ParamsException e)
{
return new ResponseParamsError(422,e.getMessage(),"UNPROCESABLE ENTITY",e.getDetails());
}
}
如上,通过使用注解@ControllerAdvice,类RestExceptionHandler就可以实现全局异常的拦截处理功能。自定义的方法handleParamsException旨在拦截ParamsException异常,一旦拦截成功后,我们可以进行各种处理操作,并且返回自己想要的结果。
其中,注解@ExceptionHandler表示要拦截的异常;注解@ResponseStatus可以指定HTTP响应的状态码;当然,注解@ResponseBody也必不可少。
这里定义了一个返回参数异常信息的值对象,用于全局异常处理进行参数处理后对结果进行封装:
/**
* 用于返回参数异常的值对象
*/
public class ResponseParamsError {
private int status;
private String message;
private String error;
private List> details;
public ResponseParamsError(int status, String message, String error, List> details) {
this.status = status;
this.message = message;
this.error = error;
this.details = details;
}
setter and getter omit...
}
- 下面对这个错误类型进行测试:
新增一个接口用于测试参数错误异常:
/**
* 根据用户年龄获取大于该年龄的用户集合
*/
@GetMapping(value = "/users/age/{age}")
@ResponseStatus(HttpStatus.OK)
public ResponseCollection getUsersOlderThan(@PathVariable("age") Integer age,Pageable pageable) throws ParamsException {
if (age>120){
List> details= new ArrayList<>();
Map map = new HashMap<>();
map.put("failed","age");
map.put("message","年龄不能超过120岁");
details.add(map);
throw new ParamsException("提交的数据格式有误",details);
}else {
Page result= userRepository.getUsersOlderThan(age,pageable);
//将结果封装到集合类结果的值对象中
ResponseCollection resp= new ResponseCollection();
resp.setTotal(result.getTotalElements());
resp.setData(result.getContent());
return resp;
}
}
可以看出,该接口是根据用户年龄获取大于该年龄的用户集合。对请求参数进行校验是非常常见的业务逻辑,就本接口而言,当年龄的参数大于120的时候,我们抛出一个参数异常(ParamsException),交个全局处理异常区处理。
向该接口发送请求数据:
localhost:8080/users/age/230
看该请求,年龄参数age大于120,此参数有误,返回错误信息,结构如下:
{
"status": 422,
"message": "提交的数据格式有误",
"error": "UNPROCESABLE ENTITY",
"details": [
{
"failed": "age",
"message": "年龄不能超过120岁"
}
]
}
3.3 需要前端路由跳转的
{
"status": 302,
"data": "http://open.wx.qq.com/oauth2/connect?appid=xdfdsddffs&ddd=xxxx"
}
注:转载请注明出处