后端响应体和状态码设计

为什么要设计统一响应体

因为系统中默认给我们提供了许多的状态码,比如404,500等。但是HTTP的状态码数量有限,而随着业务的增长,HTTP状态码无法很好地表示业务中遇到的异常情况。
那么可以通过修改响应返回的JSON数据,让其带上一些固有的字段,例如以下这样的来更好的表达业务中遇到的情况。

{
  "success": true,
  "code": 10000,
  "message": "操作成功!",
  "queryResult": {
        "siteId": "5a751fab6abb5044e0d19ea1",
        "pageId": "5a754adf6abb500ad05688d9",
  }
}

目前都是基于前后端分离的开发模式,后端主要是一个RESTful API的数据接口。前后端分离的目的是让前端与后端专注于各自擅长的领域,提高工作效率,唯一的联系就在于基于RESTful API的数据接口文档。所以接口文档的定义就特别重要。在企业开发中当接口遇到错误的时候,后端如果可以将结果状态码标记的更为详细,那么就会更利于前端开发者使用,毕竟写接口的目的也是方便前端使用,这样也可以降低前后端开发人员沟通成本。

此时我们已经明确了采用统一的响应体封装的目的了。那么如何去设计响应体呢?
首先去了解最基础的统一响应体,如下:

/**
 * 统一响应体
 * @param <T> 具体数据对象类型
 * @author zhaogot
 */
public class ResponseResult<T> implements Serializable {
    /**响应码*/
    private Integer code;
    /**响应信息*/
    private String message;
    /**具体数据*/
    private T data;

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }

设计的时候,有个建议就是采用泛型,而不是采用Object。原因是系统结合Swagger2使用时,Object可能有问题,采用泛型设计就能够读取到list中的字段信息。

状态码如何设计

此时基础的统一响应体已经设计好了,也能够使用了,接下来就是需要定义系统中的响应码了。
设计系统响应码时有以下注意点:

  • Code 不建议和 HTTP Status Code 有对应关系,这样容易产生误解。
  • 一般用 0 表示成功状态,非 0 表示失败,然后根据具体业务进行细分
  • 客户端处理响应时应该先校验 HTTP 状态码是否为 200,然后再根据具体 Code 处理业务结果

我们可以这样设计状态码类,如下:

/**
 * 全局状态码
 * @author zhaogot
 */
public class Code {
    public static final int SUCCESS_CODE =0;
    public static final String SUCCESS_MESSAGE ="成功";
}

此时的状态码虽然也能够使用,而且也满足了上述的建议。但是在实际的项目中使用中仍然存在着问题。如果项目中定义的。如果所有的项目组成员都能够按照规范去定义,此时的状态码还是便于维护的。但是如果当你发现你的系统中错误码随意定义,没有任何规范的时候,此时就很难去维护系统中的全局状态码。这时候你应该考虑下使用一个枚举全局管理下你的状态码,这对线上环境定位错误问题和后续接口文档的维护都是很有帮助的。
具体示例如下:

/**
 * 全局状态码
 * @author zhaoyi
 */
public enum Code {
    //全局状态码
    SUCCESS(10000,"操作成功!");


    //操作代码
    int code;
    //提示信息
    String message;
    /**
     * 构造方法
     * @param code
     * @param message
     */
    Code(int code, String message) {
        this.code = code;
        this.message = message;
    }

    
    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}

一个基础的enum类型的全局状态码就被定义出来了。
同时ResponseResult需要改变一下。代码如下

/**
 * 统一响应体
 * @param <T> 具体数据对象类型
 * @author zhaogot
 */
public class ResponseResult<T> implements Serializable {
    /**响应码*/
    private Integer code;
    /**响应信息*/
    private String message;
    /**具体数据*/
    private T data;

    public ResponseResult(Code code, T data) {
        this.code = code.getCode();
        this.message = code.message;
        this.data = data;
    }

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }
}

用Controller验证是否可用

现在让我们写一个Controller去测试一下上述是否可行。

User实体类

public class User {
    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

Controller

/**
 * 用于测试
 * @author zhaogot
 */
@RestController
public class TestController {

    @GetMapping("/test/query")
    @ResponseBody
    public ResponseResult<User> query(){
        User user = new User();
        user.setName("zhaogot");
        user.setAge(24);
        ResponseResult result = new ResponseResult(Code.SUCCESS,user);
        return result;
    }
}

查询结果

{
"code": 0,
"message": "操作成功!",
"data": {
"name": "zhaogot",
"age": 24
}
}

已经能够返回我们想要的数据了。我们可以在此基础上完善和优化,以方便日常使用。
先贴出代码,各自的功能,代码中都有注释。

完善后的响应体和状态码

响应体

/**
 1. 统一响应体
 2. @param <T> 具体数据对象类型
 3. @author zhaogot
 */
public class ResponseResult<T> implements Serializable {
    /**响应码*/
    private Integer code;
    /**响应信息*/
    private String message;
    /**具体数据*/
    private T data;

    public ResponseResult() {}

    public ResponseResult(Code resultCode) {
        this.code = resultCode.code();
        this.message = resultCode.message();
    }

    public ResponseResult(Code resultCode, T data) {
        this.code = resultCode.code();
        this.message = resultCode.message();
        this.data = data;
    }

    public static ResponseResult success() {
        ResponseResult result = new ResponseResult(Code.SUCCESS);
        return result;
    }

    public static ResponseResult success(Object data) {
        ResponseResult result = new ResponseResult(Code.SUCCESS,data);
        return result;
    }

    public static ResponseResult failure(Code resultCode) {
        ResponseResult result = new ResponseResult(resultCode);
        return result;
    }

    public static ResponseResult failure(Code resultCode, Object data) {
        ResponseResult result = new ResponseResult(resultCode,data);
        result.setData(data);
        return result;
    }

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }
}

状态码

/**
 4. 全局状态码
 5. @author zhaoyi
 */
public enum Code {
    //全局状态码
    SUCCESS(0,"操作成功!"),
    INVALID_PARAM(10003,"非法参数!"),
    FAIL(11111,"操作失败!"),
    UNAUTHENTICATED(10001,"此操作需要登陆系统!"),
    UNAUTHORISE(10002,"权限不足,无权操作!"),
    SERVER_ERROR(99999,"抱歉,系统繁忙,请稍后重试!");

    //操作代码
    int code;
    //提示信息
    String message;
    /**
     * 构造方法
     * @param code
     * @param message
     */
    Code(int code, String message) {
        this.code = code;
        this.message = message;
    }

    // 用于获取code和对应的message

    public int code() {
        return code;
    }
    public String message() {
        return message;
    }

    /**
     * 根据
     * @param name
     * @return
     */
    public static String getMessage(String name) {
        for (Code item : Code.values()) {
            if (item.name().equals(name)) {
                return item.message;
            }
        }
        return name;
    }

    public static Integer getCode(String name) {
        for (Code item : Code.values()) {
            if (item.name().equals(name)) {
                return item.code;
            }
        }
        return null;
    }

    @Override
    public String toString() {
        return this.name();
    }

    //校验重复的code值
    public static void main(String[] args) {
        Code[] ApiResultCodes = Code.values();
        List<Integer> codeList = new ArrayList();
        for (Code ApiResultCode : ApiResultCodes) {
            if (codeList.contains(ApiResultCode.code)) {
                System.out.println(ApiResultCode.code);
            } else {
                codeList.add(ApiResultCode.code());
            }
        }
    }
}

存在的问题

如何按照上述去设计服务端的响应体,在项目中是可以使用了,需要改变的可能就是自己去定义状态码。
但是上述的设计仍然存在着一些问题。

系统共用一个枚举

虽然一个项目里使用一个枚举定义状态码可以使用,但是太过耦合,容易造成混乱,不利于后期的项目维护。
所以在实际的项目中,我们通常是Common模块封装一个CoommonCode来定义项目通用的状态码。然后各个模块各自去新建一个枚举定义自己模块的状态码。为了保证每个枚举的结构相同,可以采用接口的方式去定义公共的状态码接口。如下:
接口

public interface ResultCode {
    //操作是否成功,true为成功,false操作失败
    boolean success();
    //操作代码
    int code();
    //提示信息
    String message();
}

公共状态码

public enum CommonCode implements ResultCode{
    // 非法参数
    INVALID_PARAM(false,10003,"非法参数!"),
    // 操作成功
    SUCCESS(true,0,"操作成功!"),
    // 操作失败
    FAIL(false,11111,"操作失败!"),
    //
    UNAUTHENTICATED(false,10001,"此操作需要登陆系统!"),
    UNAUTHORISE(false,10002,"权限不足,无权操作!"),
    SERVER_ERROR(false,99999,"抱歉,系统繁忙,请稍后重试!");
    
    //操作是否成功
    boolean success;
    //操作代码
    int code;
    //提示信息
    String message;
    private CommonCode(boolean success,int code, String message){
        this.success = success;
        this.code = code;
        this.message = message;
    }



    @Override
    public boolean success() {
        return success;
    }
    @Override
    public int code() {
        return code;
    }

    @Override
    public String message() {
        return message;
    }
}

其他模块状态码定义类似。

此时项目中的状态码已经设计好了,这时结合设计好的统一响应体已经能够在项目中去应用了。

改进

状态码此时已经分为通用的CommonCode和各个模块自己的业务Code了。响应体我们也可以这样去改进一下。以便适应更多的使用场景。

列表响应体

项目中列表是比较多见的响应数据。我们可以在系统中为列表设计一个专门的响应体。日常开发中,前端解析后端的列表数据时,通常需要两个数据:1.具体的数据用于列表展示 2.列表数据的总数
所以列表的响应体设计如下:
列表数据本身

@Data
@ToString
public class QueryResult<T> {
    /**列表数据*/
    private List<T> list;
    /**数据总数*/
    private long total;
}

列表响应体

@Data
@ToString
public class QueryResponseResult extends ResponseResult {
    /**列表数据实体*/
    QueryResult queryResult;

    /**
     * 构造方法
     * @param resultCode
     * @param queryResult
     */
    public QueryResponseResult(ResultCode resultCode,QueryResult queryResult){
        super(resultCode);
       this.queryResult = queryResult;
    }

}

设计说明
1.为了简化代码,采用了Lombok表达式
2.采用构造方法的方式,把具体数据封装到响应体内。

其他的响应体都可以这样设计。比如订单详情页的响应体

@Data
@ToString
public class OrderResult extends ResponseResult {
    private Orders Orders;
    public OrderResult(ResultCode resultCode, Orders Orders) {
        super(resultCode);
        this.Orders = Orders;
    }
}

至此整个微服务的项目响应体和状态码都设计成功了!!!!

发布了25 篇原创文章 · 获赞 3 · 访问量 2956

猜你喜欢

转载自blog.csdn.net/zhaogot/article/details/103664954