设计指南及请求返回规范

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> details;


    public ParamsException(String message, List> details) {
        super(message);
        this.details = details;
    }

    setter and getter omit...
}

新建类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"
}

注:转载请注明出处

你可能感兴趣的:(设计指南及请求返回规范)