SpringBoot实现全局通用返回功能及异常处理
前言
这个功能旨在标准化后端与前端的通信,提供一致的成功和失败响应格式。通过自定义错误码和异常处理机制,系统可以细粒度地区分和处理各种错误情况,同时对异常集中处理,确保前端获得详细且有用的错误信息。
全局通用返回对象
在项目开发中,前后端交互是重要的一环,假设一个场景:前端向后端发来一个业务请求,如果后端只返回 null 给前端,那么 null 的意思是数据为空还是请求处理错误呢?前端同学肯定一脸懵?又比如后端自己进行接口的调试,如果发现返回值有问题,那具体是哪个业务逻辑出了问题呢?后端同学估计得一步步调试去找问题。比如下面这种写法就十分不友好:
@PostMapping("/login")
public BaseResponse<User> userLogin(@RequestBody UserLoginRequest userLoginRequest) {
if (userLoginRequest == null) {
// 失败
return null;
}
// 此处省略处理逻辑
// 成功但user为null
return user;
}
显然,单单返回一个响应结果是不够的,我们还需要给返回对象扩展信息,告诉接收的人这个请求在业务层面上是成功还是失败,如果失败,那问题出在什么业务逻辑中。
这个时候我们就可以定义一个全局通用返回对象来封装这些信息,一个基本的全局通用返回对象结构如下:
// 成功
{
"code": 0, // 业务状态码,类似 HTTP 响应状态码,后面介绍
"data": {
"name": "waihai" // 返回数据
},
"message": "ok", // 错误信息
"description": "" // 错误描述
}
// 错误
{
"code": 40000,
"data": null,
"message": "请求参数错误",
"description": "密码不能超过xxxx"
}
从结构上看,我们知道响应对象有 code、data、message、description 这四个属性,那么就可以写出一个基本的全局通用返回对象类,但是在实现前我们先思考一个问题:每个属性对应的类型是什么?
code 可以是 int 类型,message 和 description 用于传递具体信息可以是 String 类型,那 data 呢?首先我们要明白我们要实现的是全局 通用 返回对象类,那不管返回的数据类型是 Integer、Long、还是 List 都应该可以设置到 data 中,那么泛型就符合我们的需求。因此,我们可以设计出下面的全局返回对象:
/**
* 全局返回对象
*
* @param <T>
*/
@Data
public class BaseResponse<T> {
// 业务状态码
private int code;
// 返回数据
private T data;
// 错误信息
private String message;
// 错误描述
private String description;
public BaseResponse(int code, T data, String message, String description) {
this.code = code;
this.data = data;
this.message = message;
this.description = description;
}
// 复用其他的构造函数,根据传递的参数不同做区分
public BaseResponse(int code, T data) {
this(code, data, "", "");
}
}
在代码中我们可以这样使用:
@PostMapping("/login")
public BaseResponse<User> userLogin(@RequestBody UserLoginRequest userLoginRequest) {
if (userLoginRequest == null) {
// 失败
return new BaseReponse<>(40000, null, "请求参数错误", "详细描述,xxxx");
}
// 此处省略处理逻辑
// 成功
// 这里定义业务状态码 0 代表成功
return new BaseReponse<>(0, user, "ok", "");
}
这样响应中就带上了具体的业务信息,null 的含义区分问题也迎刃而解。
返回工具类
但是这样写比较麻烦,每次都需要自己 new 对象,并且 message 消息手动返回,你写项目返回 “ok”,其他开发同学可能写 “yes”,格式统一也存在问题。为了解决这些问题,我们可以写一个返回工具类,帮助我们返回成功或失败的返回对象,这里我们先以成功为例:
/**
* 返回工具类
*
* @author waihai
*/
public class ResultUtils {
/**
* 成功
*
* @param data
* @param <T>
* @return
*/
// 成功时我们只需要关注返回的数据即可
public static <T> BaseResponse<T> success(T data) {
return new BaseResponse<>(0, data, "ok", "");
}
}
再次回到代码中:
@PostMapping("/login")
public BaseResponse<User> userLogin(@RequestBody UserLoginRequest userLoginRequest) {
if (userLoginRequest == null) {
// 失败
return new BaseReponse<>(40000, null, "请求参数错误", "详细描述,xxxx");
}
// 此处省略处理逻辑
// 成功
return ResultUtils.success(user);
}
这样代码是不是就清爽了许多。
自定义错误码
可能有同学会疑惑,为什么不把错误的返回操作也写了?但其实如果你照着成功返回的逻辑来写会发现,错误码需要手动返回,那么就会和前一个问题一样,格式统一存在问题,你写 “40000”,那他记岔了写了 “40001”,这咋办?并且出于规范,错误码和错误信息 message 应该是一一对应的,不能随意更改。
还有一个原因是 http 中的状态码较少,不能精准的区分问题,即不同的问题可能返回相同的状态码,比如 ”401“ 能大致知道是用户的身份验证问题,但是其中也有区别,第一种是用户登录但不是管理员所以无权限访问,第二种是用户没登录所以无权限访问,这两者是有区别的。
所以我们需要自定义一套错误码并且将基本的错误信息封装在错误码中,比如下面这个例子,使用枚举类封装了一套错误码,每个错误码有五位(成功为 0),前三位参考 http 状态码,后面两位做更细粒度的区分。
/**
* 错误码
*
* @author waihai
*/
public enum ErrorCode {
SUCCESS(0, "OK", ""),
PARAMS_ERROR(40000, "请求参数错误", ""),
NULL_ERROR(40001, "请求数据为空", ""),
NOT_LOGIN(40100, "未登录", ""),
NO_AUTH(40101, "无权限", ""),
FORBIDDEN(40301, "禁止操作", ""),
SYSTEM_ERROR(50000, "系统内部异常", "");
// 错误码
private final int code;
// 错误码信息
private final String message;
// 错误码描述(详细)
private final String description;
ErrorCode(int code, String message, String description) {
this.code = code;
this.message = message;
this.description = description;
}
// 只能读取,不能修改
public int getCode() {
return code;
}
public String getMessage() {
return message;
}
public String getDescription() {
return description;
}
}
那么我们的全局通用返回对象和返回工具类就可以直接从错误码中获取信息了,修改后:
/**
* 全局返回对象
*
* @param <T>
*/
@Data
public class BaseResponse<T> {
// 业务状态码
private int code;
// 返回数据
private T data;
// 错误信息
private String message;
// 错误描述
private String description;
public BaseResponse(int code, T data, String message, String description) {
this.code = code;
this.data = data;
this.message = message;
this.description = description;
}
// 复用其他的构造函数,根据传递的参数不同做区分
public BaseResponse(int code, T data) {
this(code, data, "", "");
}
public BaseResponse(ErrorCode errorCode) {
this(errorCode.getCode(), null, errorCode.getMessage(), errorCode.getDescription());
}
public BaseResponse(ErrorCode errorCode, String description) {
this(errorCode.getCode(), null, errorCode.getMessage(), description);
}
}
/**
* 返回工具类
*
* @author waihai
*/
public class ResultUtils {
/**
* 成功
*
* @param data
* @param <T>
* @return
*/
// 成功时我们只需要关注返回的数据即可
public static <T> BaseResponse<T> success(T data) {
return new BaseResponse<>(0, data, "ok", "");
}
// 根据传入的参数不同区分
public static BaseResponse error(ErrorCode errorCode) {
return new BaseResponse<>(errorCode);
}
public static BaseResponse error(ErrorCode errorCode, String description) {
return new BaseResponse<>(errorCode, description);
}
public static BaseResponse error(ErrorCode errorCode, String message, String description) {
return new BaseResponse<>(errorCode.getCode(), null, message, description);
}
public static BaseResponse error(int code, String message, String description) {
return new BaseResponse<>(code, null, message, description);
}
}
最后又回到代码中,可以改写为以下形式:
@PostMapping("/login")
public BaseResponse<User> userLogin(@RequestBody UserLoginRequest userLoginRequest) {
if (userLoginRequest == null) {
// 失败
return ResultUtils.error(ErrorCode.PARAMS_ERROR, "xxxx");
}
// 此处省略处理逻辑
// 成功
return ResultUtils.success(user);
}
可以发现代码变得更加规范了,也解决了我们一开始提到的三个问题:格式统一,随意更改,问题区分。
做到这一步似乎已经够了,但其实我们的全局通用返回功能还有一点问题,当业务逻辑不通过时,我们直接调用返回工具类返回,不知道大家有没有注意到,在错误码中我们定义了 SYSTEM_ERROR(50000, "系统内部异常", "");
,表示系统内部的异常如除零、NPE、StackOverflowError 等问题出现时也会返回对应的错误信息,但是事情真的是这样吗?当然不是,一旦出现系统异常,如果你没有主动捕获异常,那么出现的异常就会终止项目运行,就谈不上返回了,况且系统异常的出现的地方不可控,在哪里处理也是一个问题,重要的是,系统错误信息如果直接返回,那么项目的部分信息也会泄露出去,十分不安全,所以接下来我们实现一个全局异常处理功能解决上述问题。
自定义异常
在解决上述问题之前,我们可以对返回方式进行一个优雅的处理,将错误信息封装到了异常类中,再以异常的形式抛出,通过全局异常处理器来统一返回,其中也包含了我们提到的系统异常,具体代码如下:
/**
* 自定义异常类
*
* @author waihai
*/
public class BusinessException extends RuntimeException{
// 错误码
private final int code;
// 错误码信息
private final String description;
// 复用其他的构造函数,根据传递的参数不同做区分
public BusinessException(String message, int code, String description) {
super(message);
this.code = code;
this.description = description;
}
public BusinessException(ErrorCode errorCode, String description) {
super(errorCode.getMessage());
this.code = errorCode.getCode();
this.description = description;
}
public BusinessException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.code = errorCode.getCode();
this.description = errorCode.getDescription();
}
// 获取错误码
public int getCode() {
return code;
}
// 获取错误码信息
public String getDescription() {
return description;
}
}
可以看到我们继承了 RuntimeException 类,由于 RuntimeException 中只有 message 字段,我们扩充了 code,description 这两个字段来传递错误信息。
那在项目中我们就可以手动抛出异常,之后再由全局异常处理器捕获异常,最后统一返回。
@PostMapping("/login")
public BaseResponse<User> userLogin(@RequestBody UserLoginRequest userLoginRequest) {
if (userLoginRequest == null) {
// 失败
throw new BusinessException(ErrorCode.PARAMS_ERROR, "xxxx");
}
// 此处省略处理逻辑
// 成功
return ResultUtils.success(user);
}
全局异常处理器
写之前先简单介绍一下,在我们实现的功能中,全局异常处理器有以下作用:捕获代码中所有的异常,返回详细的业务报错/信息;屏蔽项目框架本身的异常,不暴露服务器内部状态;对异常进行集中处理,如统一日志记录。当然还有很多其他功能,这里只列举部分。
/**
* 全局异常处理器
*
* @author waihai
*/
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
// 处理自定义异常
@ExceptionHandler(BusinessException.class)
public BaseResponse businessException(BusinessException e) {
log.error("businessException:" + e.getMessage(), e);
return ResultUtils.error(e.getCode(), e.getMessage(), e.getDescription());
}
// 处理系统异常
@ExceptionHandler(RuntimeException.class)
public BaseResponse runtimeException(BusinessException e) {
log.error("runtimeException:" + e.getMessage(), e);
return ResultUtils.error(ErrorCode.SYSTEM_ERROR, e.getMessage(),"");
}
}
这里直接让 AI 生成一份简单的介绍:
GlobalExceptionHandler 类
- 这个类通过使用
@RestControllerAdvice
注解,被标记为一个全局异常处理器,处理所有控制器中的异常。
businessException 方法
@ExceptionHandler(BusinessException.class)
:这个注解表示该方法会处理所有BusinessException
异常。- 参数
BusinessException e
:这是捕获的异常对象。 log.error("businessException:" + e.getMessage(), e);
:记录异常的错误信息,包含异常消息和堆栈跟踪。return ResultUtils.error(e.getCode(), e.getMessage(), e.getDescription());
:通过ResultUtils
工具类创建一个错误响应对象,包含异常的代码、消息和描述。
runtimeException 方法
@ExceptionHandler(RuntimeException.class)
:这个注解表示该方法会处理所有RuntimeException
异常。- 参数
RuntimeException e
:这是捕获的异常对象。 log.error("runtimeException:" + e.getMessage(), e);
:记录异常的错误信息,包含异常消息和堆栈跟踪。return ResultUtils.error(ErrorCode.SYSTEM_ERROR, e.getMessage(),"");
:通过ResultUtils
工具类创建一个错误响应对象,使用系统错误码ErrorCode.SYSTEM_ERROR
,包含异常消息和一个空描述。
那么回到代码中,这个全局异常处理器可以捕获我们手动抛出的异常,也可以处理系统异常,之后集中处理异常,记录错误信息,最后以统一格式返回。
总结
这篇文章简单的介绍了 SpringBoot 中如何实现全局通用返回功能及异常处理,主要通过五个部分来实现,包括:全局通用返回对象、返回工具类、自定义错误码、自定义异常、全局异常处理器。
一个基本的异常处理流程如下:
分享到这里就结束了,欢迎大家在评论区探讨交流!!!