实现优雅的数据返回 - springboot

为什么要统一返回值

定义统一的数据返回格式有利于提高开发效率、降低沟通成本,降低调用方的开发成本。目前比较流行的是基于JSON格式的数据交互。无论是HTTP接口还是RPC接口,保持返回值格式统一很重要。

一般情况下,统一返回数据格式没有固定的规范,只要能描述清楚返回的数据状态以及要返回的具体数据即可,但是一般会包含状态码、消息提示语、具体数据这3部分内容。

{
    
    
  "code": 20000,
  "message": "成功",
  "data": {
    
    
    "items": [
      {
    
    
        "id": "1",
        "name": "weiz",
        "intro": "备注"
      }
    ]
  }
}

定义的返回值包含4要素:响应结果、响应码、消息、返回数据。


统一数据返回

数据格式

定义的返回值包含如下内容:

  • Integer code:成功时返回0,失败时返回具体错误码。
  • String message:成功时返回null,失败时返回具体错误消息。
  • T data:成功时返回具体值,失败时为null。
    • data字段为泛型字段,根据实际的业务返回前端需要的数据类型。
{
    
    
  "code": 20000,
  "message": "成功",
  "data": {
    
    
    "items": [
      {
    
    
        "id": "1",
        "name": "weiz",
        "intro": "备注"
      }
    ]
  }
}

状态码

返回的数据中有一个非常重要的字段:状态码。状态码字段能够让服务端、客户端清楚知道操作的结果、业务是否处理成功,如果失败,失败的原因等信息。

状态码 含义 说明
200 OK 请求成功
201 CREATED 创建成功
204 DELETED 删除成功
400 BAD REQUEST 请求的地址不存在或者包含不支持的参数
401 UNAUTHORIZED 未授权(验证不通过)
403 FORBIDDEN 被禁止访问
404 NOT FOUND 请求的资源不存在
406 Not acceptable 错误 – 无法接受
422 Unprocesable entity [POST/PUT/PATCH] 当创建一个对象时,发生一个验证错误
500 INTERNAL SERVER ERROR 内部错误

其他的业务相关状态码需要根据实际业务定义。

定义数据处理类

视图对象(响应数据结构):

package com.qsdbl.malldemo.entity.vo;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

/**
 * @description vo - 视图对象
 * @author: 轻率的保罗
 * @since: 2022-11-26
 * @version V1.0
 */
@Data
@ApiModel("响应数据结构")
public class DataVo<Object> {
    
    
    @ApiModelProperty("响应业务状态")
    private Integer code;
    @ApiModelProperty("响应消息。一般为请求处理失败后返回的错误提示。")
    private String msg;
    @ApiModelProperty("响应中的数据")
    private Object data;

    public DataVo(Integer code, String msg, Object data) {
    
    
        this.code = code;
        this.msg = msg;
        this.data = data;
    }

    public DataVo(Object data) {
    
    
        this.code = 200;
        this.msg = "OK";
        this.data = data;
    }

    public DataVo() {
    
    

    }
}

结果处理类:

package com.qsdbl.malldemo.utils;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.qsdbl.malldemo.entity.vo.DataVo;
import java.util.List;

/**
 * @Description:
 * 状态码说明
 *    200:表示成功
 *    500:表示错误,错误信息在msg字段中
 *    501:bean验证错误,无论多少个错误都以map形式返回
 *    502:拦截器拦截到用户token出错
 *    555:异常抛出信息
 *
 * @author: 轻率的保罗
 * @since: 2022-11-26
 * @version V1.0
 */
public class JSONResult {
    
    

    // 定义jackson对象
    private static final ObjectMapper MAPPER = new ObjectMapper();

    /**
     * 自定义状态信息
     */
    public static DataVo build(Integer code, String msg, Object data) {
    
    
        return new DataVo(code, msg, data);
    }

    /**
     * 正常 代码200
     */
    public static DataVo ok(Object data) {
    
    
        return new DataVo(data);
    }

    /**
     * 正常 代码200
     */
    public static DataVo ok() {
    
    
        return new DataVo(null);
    }

    /**
     * 异常 代码500
     * 普通异常,返回错误信息
     */
    public static DataVo errorMsg(String msg) {
    
    
        return new DataVo(500, msg, null);
    }

    /**
     * 异常 代码501
     * 参数异常,map中放具体的异常参数(key/value)。例如 email = 邮箱格式不正确!
     */
    public static DataVo errorMap(Object data) {
    
    
        return new DataVo(501, "error", data);
    }

    /**
     * Token异常 代码502
     */
    public static DataVo errorTokenMsg(String msg) {
    
    
        return new DataVo(502, msg, null);
    }

    /**
     * 异常 代码555
     * 系统Exception异常,在try-catch中使用
     */
    public static DataVo errorException(String msg) {
    
    
        return new DataVo(555, msg, null);
    }

    /**
     * 将json字符串转化为DataVo对象。
     * (data字段的值为对象)需要转换的对象是一个类
     * @param jsonData json字符串
     * @param clazz data的值对应的实体类(例如:用户实体类,SysUserEntity.class)
     */
    public static DataVo formatToPojo(String jsonData, Class<?> clazz) {
    
    
        try {
    
    
            if (clazz == null) {
    
    
                return MAPPER.readValue(jsonData, DataVo.class);
            }
            JsonNode jsonNode = MAPPER.readTree(jsonData);
            JsonNode data = jsonNode.get("data");
            Object obj = null;
            if (clazz != null) {
    
    
                if (data.isObject()) {
    
    
                    obj = MAPPER.readValue(data.traverse(), clazz);
                } else if (data.isTextual()) {
    
    
                    obj = MAPPER.readValue(data.asText(), clazz);
                }
            }
            return build(jsonNode.get("code").intValue(), jsonNode.get("msg").asText(), obj);
        } catch (Exception e) {
    
    
            return null;
        }
    }

    /**
     * 将json字符串转化为DataVo对象。
     * data字段的值为空,无该字段、为空字符串、空对象、空数组等
     * @param json json字符串
     */
    public static DataVo format(String json) {
    
    
        try {
    
    
            return MAPPER.readValue(json, DataVo.class);
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 将json字符串转化为DataVo对象。
     * (data字段的值为数组)需要转换的对象是一个list
     * @param jsonData json字符串
     * @param clazz 数组中数据对应的实体类(例如:数组中保存多个用户数据,用户实体类,SysUserEntity.class)
     */
    public static DataVo formatToList(String jsonData, Class<?> clazz) {
    
    
        try {
    
    
            JsonNode jsonNode = MAPPER.readTree(jsonData);
            JsonNode data = jsonNode.get("data");
            Object obj = null;
            if (data.isArray() && data.size() > 0) {
    
    
                obj = MAPPER.readValue(data.traverse(),
                        MAPPER.getTypeFactory().constructCollectionType
                                (List.class, clazz));
            }
            return build(jsonNode.get("code").intValue(), jsonNode.get("msg").asText(), obj);
        } catch (Exception e) {
    
    
            return null;
        }
    }
}

测试

返回数据

定义数据处理类后,在控制器中将返回的数据统一加上数据处理。调用如下:

/**
 * 用户表 Mapper对象
 */
@Autowired
private SysUserMapper userMapper;

@ApiOperation("查询所有用户数据")
@GetMapping("/all")
public DataVo queryAll(){
    
    
        return JSONResult.ok(userMapper.selectList(null));
}

响应内容:

{
    
    
  "code": 200,
  "msg": "OK",
  "data": [...]
}

其他方法使用示例

(返回数据)json字符串 转换成 DataVo对象:

package com.qsdbl.malldemo.resultTest;

import com.qsdbl.malldemo.entity.SysUserEntity;
import com.qsdbl.malldemo.entity.vo.DataVo;
import com.qsdbl.malldemo.utils.JSONResult;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;

/**
 * @author: 轻率的保罗
 * @since: 2022-11-26
 * @Description: 测试 json字符串 转换成 DataVo对象
 */
public class MyTest01 {
    
    

    //data字段值为 数组
    String list1 = "{\n" +
            "  \"code\": 200,\n" +
            "  \"msg\": \"OK\",\n" +
            "  \"data\": [\n" +
            "    {\n" +
            "      \"userCode\": \"admin\",\n" +
            "      \"userName\": \"管理员\",\n" +
            "      \"userPwd\": \"12345\",\n" +
            "      \"memo\": \"测试数据!\",\n" +
            "      \"deleted\": 0\n" +
            "    }\n" +
            "  ]\n" +
            "}";

    //data字段值为 对象
    String obj = "{\n" +
            "\t\"code\": 200,\n" +
            "\t\"msg\": \"OK\",\n" +
            "\t\"data\": {\n" +
            "\t\t\"userCode\": \"admin\",\n" +
            "\t\t\"userName\": \"管理员\",\n" +
            "\t\t\"userPwd\": \"12345\",\n" +
            "\t\t\"memo\": \"测试数据!\",\n" +
            "\t\t\"deleted\": 0\n" +
            "\t}\n" +
            "}\n";

    //data字段值无数据:
    //data字段值为 空字符串
    String datanull = "{\n" +
            "\t\"code\": 200,\n" +
            "\t\"msg\": \"OK\",\n" +
            "\t\"data\": \"\"\n" +
            "}\n";

    //data字段值为 空对象
    String datanull2 = "{\n" +
            "\t\"code\": 200,\n" +
            "\t\"msg\": \"OK\",\n" +
            "\t\"data\": {}\n" +
            "}\n";

    //没有data字段
    String datanull3 = "{\n" +
            "\t\"code\": 200,\n" +
            "\t\"msg\": \"OK\"\n" +
            "}\n";

    @Test
    void test01(){
    
    
        System.out.println("---json字符串 转换成 DataVo对象:\n");
        System.out.println("---data的值为一个数组,数组中存放的是用户数据(用户实体类)");
        DataVo dataVo = JSONResult.formatToList(list1,SysUserEntity.class);
        System.out.println(dataVo);
        System.out.println("用户数量:"+((ArrayList)dataVo.getData()).size());
//        System.out.println("用户1:"+((ArrayList)dataVo.getData()).get(0));


        System.out.println("\n---data的值为一个对象,用户数据(用户实体类)");
        DataVo dataVo_obj = JSONResult.formatToPojo(obj,SysUserEntity.class);
        System.out.println(dataVo_obj);


        System.out.println("\n---data的值为空(或空对象、空数组等)");
        //方式一:
        DataVo dataVo_null = JSONResult.format(datanull);
        System.out.println(dataVo_null);
        //方式二:
        DataVo dataVo_null2 = JSONResult.formatToPojo(datanull2,null);
        System.out.println(dataVo_null2);
        DataVo dataVo_null3 = JSONResult.formatToPojo(datanull3,null);
        System.out.println(dataVo_null3);
    }
}

运行结果:

---json字符串 转换成 DataVo对象:

---data的值为一个数组,数组中存放的是用户数据(用户实体类)
DataVo(code=200, msg=OK, data=[SysUserEntity{
    
    userCode='admin', userName='管理员', userPwd='12345', memo='测试数据!', deleted=0}])
用户数量:1

---data的值为一个对象,用户数据(用户实体类)
DataVo(code=200, msg=OK, data=SysUserEntity{
    
    userCode='admin', userName='管理员', userPwd='12345', memo='测试数据!', deleted=0})

---data的值为空(或空对象、空数组等)
DataVo(code=200, msg=OK, data=)
DataVo(code=200, msg=OK, data={
    
    })
DataVo(code=200, msg=OK, data=null)

进程已结束,退出代码0

全局异常处理

Spring Boot框架的异常处理有多种方式,从范围来说,包括全局异常捕获处理方式局部异常捕获处理方式。下面介绍3种比较常用的异常处理解决方案。

  • (1)使用@ExceptionHandler处理局部异常
    • 在控制器中通过加入@ExceptionHandler注解的方法来实现异常的处理。这种方式非常容易实现,但是只能处理使用@ExceptionHandler注解方法的控制器异常,而无法处理其他控制器的异常,所以不推荐使用。
  • (2)配置SimpleMappingExceptionResolver类来处理异常
    • 通过配置SimpleMappingExceptionResolver类实现全局异常的处理,但是这种方式不能针对特定的异常进行特殊处理,所有的异常都按照统一的方式处理。
  • (3)使用RestControllerAdvice注解处理全局异常
    • 使用@RestControllerAdvice、@ExceptionHandler注解实现全局异常处理,@RestControllerAdvice定义全局异常处理类,@ExceptionHandler指定自定义错误处理方法拦截的异常类型。实现全局异常捕获,并针对特定的异常进行特殊处理。

推荐使用@RestControllerAdvice注解方式处理全局异常,这样可以针对不同的异常分开处理。

示例

package com.qsdbl.malldemo.configuration;

import com.qsdbl.malldemo.utils.JSONResult;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * @author: 轻率的保罗
 * @since: 2022-11-26
 * @Description: 自定义异常处理类
 */
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler  {
    
    

    /**
     * 处理全部Exception的异常(代码中try-catch捕获处理了的此处不会处理)
     * 如果需要处理其他异常,例如NullPointerException异常,则只需要在GlobalException类中使用@ExceptionHandler(value ={NullPointerException.class})注解重新定义一个异常处理的方法即可。
     */
    @ExceptionHandler(value = {
    
    Exception.class })
    public Object errorHandler(HttpServletRequest reqest,
                               HttpServletResponse response, Exception e) throws Exception {
    
    
        //e.printStackTrace();
        // 记录日志
        log.error(ExceptionUtils.getMessage(e));
        return JSONResult.errorException("服务器异常,"+ExceptionUtils.getMessage(e));
    }
      
     /**
     * 404异常处理
     */
    @ExceptionHandler(NoHandlerFoundException.class)
    public Object handlerNoFoundException(NoHandlerFoundException e) {
    
    
        String message = ExceptionUtils.getMessage(e);
        // 记录日志
        log.error(message);
        return JSONResult.build(404,"您访问的api【"+message.substring(message.indexOf("/"))+"】不存在!!!",null);
    }
}

上面的示例,处理全部Exception的异常,如果需要处理其他异常,例如NullPointerException异常,则只需要在GlobalException类中使用@ExceptionHandler(value ={NullPointerException.class})注解重新定义一个异常处理的方法即可。

要捕获404异常还需要在springboot配置文件进行如下配置:

#当没有对应处理器时,允许抛出异常(让自定义的异常处理类捕获404)
spring.mvc.throw-exception-if-no-handler-found=true
#禁用静态资源规则(不为工程中的资源文件建立映射)
spring.web.resources.add-mappings=false

#上述配置:所有访问不到的路径,均会抛出404异常,捕获之后返回自定义的DataVo实例-json对象。
#将不能访问static下的静态资源(若部署前端项目将不能访问),得前后端分开部署。

要(通过web)访问项目中的静态资源(部署网站、图片等资源),解决方法:在自定义配置类中添加相关的ResourceHandler。代码实现见这篇博客:springboot笔记-静态资源相关问题


测试1

//添加一处错误代码
int i = 4/0;

响应内容:

{
    
    
  "code": 555,
  "msg": "服务器异常,ArithmeticException: / by zero",
  "data": null
}

默认返回的是满屏的错误信息,自定义全局异常处理类之后,返回我们指定格式的信息。


测试2

注意:若使用try-catch捕获处理了,则上边定义的全局异常处理类不会处理。

try{
    
    
    int i = 4/0;
}catch (Exception e){
    
    
    return JSONResult.errorException("算术异常!!!");
}

响应内容:

{
    
    
  "code": 555,
  "msg": "算术异常!!!",
  "data": null
}

响应内容是try-catch中使用JSONResult.errorException返回的错误信息!


说明

本博客中的案例,使用的maven依赖如下:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.5.2</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

<!--        启用web支持-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!--        启用lombok(lombok 与 日志@Slf4j)-->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

<!--        启用单元测试-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

<!-- mybatis-plus -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.2</version>
</dependency>


笔记摘自:《Spring Boot从入门到实战》-章为忠

猜你喜欢

转载自blog.csdn.net/weixin_44773109/article/details/128051699