欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

RestFul API 统一格式返回 + 全局异常处理

程序员文章站 2023-02-21 13:54:11
一、背景 在分布式、微服务盛行的今天,绝大部分项目都采用的微服务框架,前后端分离方式。前端和后端进行交互,前端按照约定请求 路径,并传入相关参数,后端服务器接收请求,进行业务处理,返回数据给前端。 所以统一接口的返回值,保证接口返回值的幂等性很重要,本文主要介绍博主当前使用的结果集。 二、统一格式设 ......

一、背景

在分布式、微服务盛行的今天,绝大部分项目都采用的微服务框架,前后端分离方式。前端和后端进行交互,前端按照约定请求url路径,并传入相关参数,后端服务器接收请求,进行业务处理,返回数据给前端。

所以统一接口的返回值,保证接口返回值的幂等性很重要,本文主要介绍博主当前使用的结果集。

二、统一格式设计

2.1 统一结果的一般形式

  • 示例:
{
	# 是否响应成功
	success: true,
	# 响应状态码
	code: 200,		
	# 响应数据
	data: object
	# 返回错误信息
	message: "",
}

2.2 结果类枚举

public enum resultcodeenum {
    /*** 通用部分 100 - 599***/
    // 成功请求
    success(200, "successful"),
    // 重定向
    redirect(301, "redirect"),
    // 资源未找到
    not_found(404, "not found"),
    // 服务器错误
    server_error(500,"server error"),

    /*** 这里可以根据不同模块用不同的区级分开错误码,例如:  ***/

    // 1000~1999 区间表示用户模块错误
    // 2000~2999 区间表示订单模块错误
    // 3000~3999 区间表示商品模块错误
    // 。。。

    ;
    /**
     * 响应状态码
     */
    private integer code;
    /**
     * 响应信息
     */
    private string message;

    resultcodeenum(integer code, string msg) {
        this.code = code;
        this.message = msg;
    }

    public integer getcode() {
        return code;
    }

    public string getmessage() {
        return message;
    }
}
  • code:响应状态码

一般小伙伴们是在开发的时候需要什么,就添加什么。但是,为了规范,我们应当参考http请求返回的状态码。

code区间 类型 含义
1** 100-199 信息 服务器接收到请求,需要请求者继续执行操作
2** 200-299 成功 请求被成功接收并处理
3** 300-399 重定向 需要进一步的操作以完成请求
4** 400-499 客户端错误 请求包含语法错误或无法完成请求
5** 500-599 服务器错误 服务器在处理的时候发生错误

常见的http状态码:

  1. 200 - 请求成功;
  2. 301 - 资源(网页等)被永久转移到其它url
  3. 404 - 请求的资源(网页等)不存在;
  4. 500 - 内部服务器错误。
  • message:错误信息

在发生错误时,如何友好的进行提示?

  1. 根据code 给予对应的错误码定位;
  2. 把错误描述记录到message中,便于接口调用者更详细的了解错误。

2.3 统一结果类

public class httpresult <t> implements serializable {

    /**
     * 是否响应成功
     */
    private boolean success;
    /**
     * 响应状态码
     */
    private integer code;
    /**
     * 响应数据
     */
    private t data;
    /**
     * 错误信息
     */
    private string message;

    // 构造器开始
    /**
     * 无参构造器(构造器私有,外部不可以直接创建)
     */
    private httpresult() {
        this.code = 200;
        this.success = true;
    }
    /**
     * 有参构造器
     * @param obj
     */
    private httpresult(t obj) {
        this.code = 200;
        this.data = obj;
        this.success = true;
    }

    /**
     * 有参构造器
     * @param resultcode
     */
    private httpresult(resultcodeenum resultcode) {
        this.success = false;
        this.code = resultcode.getcode();
        this.message = resultcode.getmessage();
    }
    // 构造器结束

    /**
     * 通用返回成功(没有返回结果)
     * @param <t>
     * @return
     */
    public static<t> httpresult<t> success(){
        return new httpresult();
    }

    /**
     * 返回成功(有返回结果)
     * @param data
     * @param <t>
     * @return
     */
    public static<t> httpresult<t> success(t data){
        return new httpresult<t>(data);
    }

    /**
     * 通用返回失败
     * @param resultcode
     * @param <t>
     * @return
     */
    public static<t> httpresult<t> failure(resultcodeenum resultcode){
        return  new httpresult<t>(resultcode);
    }

    public boolean getsuccess() {
        return success;
    }

    public void setsuccess(boolean success) {
        this.success = success;
    }

    public integer getcode() {
        return code;
    }

    public void setcode(integer code) {
        this.code = code;
    }

    public t getdata() {
        return data;
    }

    public void setdata(t data) {
        this.data = data;
    }

    public string getmessage() {
        return message;
    }

    public void setmessage(string message) {
        this.message = message;
    }

    @override
    public string tostring() {
        return "httpresult{" +
                "success=" + success +
                ", code=" + code +
                ", data=" + data +
                ", message='" + message + '\'' +
                '}';
    }
}

说明:

  1. 构造器私有,外部不可以直接创建;
  2. 只可以调用统一返回类的静态方法返回对象;
  3. success 是一个boolean 值,通过这个值,可以直接观察到该次请求是否成功;
  4. data 表示响应数据,用于请求成功后,返回客户端需要的数据。

三、测试及总结

3.1 简单的接口测试

@restcontroller
@requestmapping("/httprest")
@api(tags = "统一结果测试")
public class httprestcontroller {

    @apioperation(value = "通用返回成功(没有返回结果)", httpmethod = "get")
    @getmapping("/success")
    public httpresult success(){
        return httpresult.success();
    }

    @apioperation(value = "返回成功(有返回结果)", httpmethod = "get")
    @getmapping("/successwithdata")
    public httpresult successwithdata(){
        return httpresult.success("风尘博客");
    }

    @apioperation(value = "通用返回失败", httpmethod = "get")
    @getmapping("/failure")
    public httpresult failure(){
        return httpresult.failure(resultcodeenum.not_found);
    }

}

这里 swagger以及springmvc的配置就没贴出来了,详见github 示例代码。

3.2 返回结果

{
  "code": 200,
  "success": true
}
{
  "code": 200,
  "data": "风尘博客",
  "success": true
}
{
  "code": 404,
  "message": "not found",
  "success": false
}

四、全局异常处理

使用统一返回结果时,还有一种情况,就是程序的报错是由于运行时异常导致的结果,有些异常是我们在业务中抛出的,有些是无法提前预知。

因此,我们需要定义一个统一的全局异常,在controller捕获所有异常,并且做适当处理,并作为一种结果返回。

4.1 设计思路:

  1. 自定一个异常类(如:tokenverificationexception),捕获针对项目或业务的异常;
  2. 使用@exceptionhandler注解捕获自定义异常和通用异常;
  3. 使用@controlleradvice集成@exceptionhandler的方法到一个类中;
  4. 异常的对象信息补充到统一结果枚举中;

4.2 自定义异常

public class tokenverificationexception extends runtimeexception {

    /**
     * 错误码
     */
    protected integer code;

    protected string msg;

    public integer getcode() {
        return code;
    }

    public string getmsg() {
        return msg;
    }

    public void setmsg(string msg) {
        this.msg = msg;
    }

    /**
     * 有参构造器,返回码在枚举类中,这里可以指定错误信息
     * @param msg
     */
    public tokenverificationexception(string msg) {
        super(msg);
    }
}

4.3 统一异常处理器

@controlleradvice注解是一种作用于控制层的切面通知(advice),能够将通用的@exceptionhandler@initbinder@modelattributes方法收集到一个类型,并应用到所有控制器上。

@restcontrolleradvice
@slf4j
public class globalexceptionhandler {

    /**
     * 异常捕获
     * @param e 捕获的异常
     * @return 封装的返回对象
     **/
    @exceptionhandler(exception.class)
    public httpresult handlerexception(exception e) {
        resultcodeenum resultcodeenum;
        // 自定义异常
        if (e instanceof tokenverificationexception) {
            resultcodeenum = resultcodeenum.token_verification_error;
            resultcodeenum.setmessage(getconstraintviolationerrmsg(e));
            log.error("tokenverificationexception:{}", resultcodeenum.getmessage());
        }else {
            // 其他异常,当我们定义了多个异常时,这里可以增加判断和记录
            resultcodeenum = resultcodeenum.server_error;
            resultcodeenum.setmessage(e.getmessage());
            log.error("common exception:{}", json.tojsonstring(e));
        }
        return httpresult.failure(resultcodeenum);
    }

    /**
     * 获取错误信息
     * @param ex
     * @return
     */
    private string getconstraintviolationerrmsg(exception ex) {
        // validtest1.id: id必须为正数
        // validtest1.id: id必须为正数, validtest1.name: 长度必须在有效范围内
        string message = ex.getmessage();
        try {
            int startidx = message.indexof(": ");
            if (startidx < 0) {
                startidx = 0;
            }
            int endidx = message.indexof(", ");
            if (endidx < 0) {
                endidx = message.length();
            }
            message = message.substring(startidx, endidx);
            return message;
        } catch (throwable throwable) {
            log.info("ex caught", throwable);
            return message;
        }
    }
}
  • 说明
  1. 我使用的是@restcontrolleradvice ,等同于@controlleradvice + @responsebody
  2. 错误枚举类这里省略了,详见github代码

五、测试及总结

5.1 测试接口

@restcontroller
@requestmapping("/exception")
@api(tags = "异常测试接口")
public class exceptionrestcontroller {

    @apioperation(value = "业务异常(token 异常)", httpmethod = "get")
    @getmapping("/token")
    public httpresult token() {
        // 模拟业务层抛出 token 异常
        throw new tokenverificationexception("token 已经过期");
    }


    @apioperation(value = "其他异常", httpmethod = "get")
    @getmapping("/errorexception")
    public httpresult errorexception() {
        //这里故意造成一个其他异常,并且不进行处理
        integer.parseint("abc123");
        return httpresult.success();
    }
}

5.2 返回结果

{
  "code": 500,
  "message": "for input string: \"abc123\"",
  "success": false
}
{
  "code": 4000,
  "message": "token 已经过期",
  "success": false
}

5.3 小结

@restcontrolleradvice@exceptionhandler会捕获所有rest接口的异常并封装成我们定义的httpresult的结果集返回,但是:处理不了拦截器里的异常

六、总结

没有哪一种方案是适用于各种情况的,如:分页情况,还可以增加返回分页结果的静态方案,具体实现,这里就不展示了。所以,适合自己的,具有一定可读性都是很好的,欢迎持不同意见的大佬给出意见建议。

6.1 示例代码

github 示例代码

6.2 技术交流

  1. github