为什么要统一返回值
定义统一的数据返回格式有利于提高开发效率、降低沟通成本,降低调用方的开发成本。目前比较流行的是基于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。
测试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>