为什么要设计统一响应体
因为系统中默认给我们提供了许多的状态码,比如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;
}
}
至此整个微服务的项目响应体和状态码都设计成功了!!!!