SpringBoot统一标准响应格式及异常处理
一、概述
在开发SpringBoot后端服务时,一般需要给前端统一的响应格式及异常处理,方便前端调试及配置错误提示等。比如:自定义Response结构,若每个开发者封装各自的Response结构,造成不一致,不利于前端处理,因此我们需要将响应格式统一起来,定义一个统一的标准响应格式。
二、全局统一响应
2.1、定义响应标准格式
一般至少如下三点:
1、code:响应状态码,由后端统一定义;
2、message:响应的消息;
3、data:响应返回数据。
例如:
json
{
"code": "C0001",
"message": "该用户已存在",
"data": null
}
2.2、定义统一响应对象
java
/**
* @Description: TODO:定义一统一的响应对象类
* @Author: yyalin
* @CreateDate: 2022/10/26 18:09
* @Version: V1.0
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@ApiModel(value = "定义一统一的响应对象")
public class ResultVO<T> implements Serializable {
private static final long serialVersionUID = -2548645345465031121L;
@ApiModelProperty(value = "响应状态码")
private Integer code;
@ApiModelProperty(value = "响应的消息")
private String message;
@ApiModelProperty(value = "响应返回数据")
private T data;
/**
* 功能描述:定义统一返回数据
* @MethodName: success
* @MethodParam: [data]
* @Return: com.wonders.common.res.ResultVO
* @Author: yyalin
* @CreateDate: 2023/7/16 10:38
*/
public static <T> ResultVO success(T data){
ResultVO resultVO = new ResultVO(
AppHttpCodeEnum.SUCCESS.getCode(),
AppHttpCodeEnum.SUCCESS.getMessage(),
data);
return resultVO;
}
/**
* 功能描述:定义统一返回数据并自定义一message消息
* @MethodName: success
* @MethodParam: [data]
* @Return: com.wonders.common.res.ResultVO
* @Author: yyalin
* @CreateDate: 2023/7/16 10:38
*/
public static <T> ResultVO success(String message,T data){
ResultVO resultVO = new ResultVO(
AppHttpCodeEnum.SUCCESS.getCode(),
message,
data);
return resultVO;
}
/**
* 功能描述:抛出异常 返回错误信息
* @MethodName: fail
* @MethodParam: [message]
* @Return: com.wonders.common.res.ResultVO<T>
* @Author: yyalin
* @CreateDate: 2023/7/16 10:55
*/
public static <T> ResultVO<T> fail(String message) {
return new ResultVO<T>(
AppHttpCodeEnum.FAIL.getCode(),
message,
null);
}
}
2.3、定义响应状态码
java
/**
* @Description: TODO:定义响应状态码
* @Author: yyalin
* @CreateDate: 2023/7/16 10:14
* @Version: V1.0
*/
@Getter
@AllArgsConstructor
public enum AppHttpCodeEnum {
// 成功
SUCCESS(200, "操作成功"),
FAIL(300,"操作失败"),
// 登录
NEED_LOGIN(401, "需要登录后操作"),
USERNAME_EXIST(501, "用户名已存在");
private Integer code; //响应状态码
private String message; //响应的消息
}
2.4、controller层使用
java
@ApiOperation(value="根据id获取学生信息", notes="getStudentInfo")
@GetMapping("/{studentId}")
public ResultVO getStudentInfo(@PathVariable long studentId){
//1、校验传来的参数是不是为空或者异常
return ResultVO.success(studentService.selectById(studentId));
}
三、统一异常处理
**例如:**我们创建了三种自定义异常类:ClientException(客户端异常)、BusinessException(业务逻辑异常)和RemoteException(第三方服务异常)。这些异常类都继承自AbstractException,这是一个抽象的基类
3.1、自定义抽象异常
java
/**
* @Description: TODO:自定义异常的抽象类
* @Author: yyalin
* @CreateDate: 2023/7/16 11:04
* @Version: V1.0
*/
@Getter
public abstract class AbstractException extends RuntimeException{
private final Integer code;
private final String message;
public AbstractException(AppHttpCodeEnum appHttpCodeEnum, String message,
Throwable throwable){
super(message,throwable);
this.code = appHttpCodeEnum.getCode();
this.message = Optional.ofNullable(message).orElse(appHttpCodeEnum.getMessage());
}
}
3.2、具体的业务处理异常
java
/**
* @desc:定义具体的业务异常处理
* @author :yyalin
* @date:2019-11-27
*/
public class BusinessException extends AbstractException {
public BusinessException(AppHttpCodeEnum appHttpCodeEnum, String message, Throwable throwable) {
super(appHttpCodeEnum, message, throwable);
}
public BusinessException(AppHttpCodeEnum appHttpCodeEnum) {
this(appHttpCodeEnum, null, null);
}
public BusinessException(AppHttpCodeEnum appHttpCodeEnum,String message) {
this(appHttpCodeEnum, message, null);
}
}
3.3、全局异常处理
SpringBoot提供了一个特殊的注解@RestControllerAdvice,允许我们创建全局异常处理类并且可以定义处理各种类型异常的方法。
主要有以下三种异常:
1、MethodArgumentNotValidException:处理参数验证异常,并提供清晰的错误信息。
2、AbstractException:处理之前定义的自定义异常。
3、Throwable:作为最后的兜底,拦截所有其他异常。
java
/**
* @desc:全局异常处理类
* @author :yyalin
* @date:2019-11-27
*/
@RestControllerAdvice
@Slf4j
//@ControllerAdvice
public class GlobalExceptionHandler {
//1、处理参数验证异常 MethodArgumentNotValidException
@SneakyThrows
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public ResultVO handleValidException(HttpServletRequest request, MethodArgumentNotValidException ex) {
BindingResult bindingResult = ex.getBindingResult();
FieldError firstFieldError = CollectionUtil.getFirst(bindingResult.getFieldErrors());
String exceptionStr = Optional.ofNullable(firstFieldError)
.map(FieldError::getDefaultMessage)
.orElse(StrUtil.EMPTY);
log.error("[{}] {} [ex] {}", request.getMethod(),"URL:", exceptionStr);
return ResultVO.fail(exceptionStr);
}
// 处理自定义异常:AbstractException
@ExceptionHandler(value = {AbstractException.class})
public ResultVO handleAbstractException(HttpServletRequest request, AbstractException ex) {
String requestURL = "URL地址";
log.error("[{}] {} [ex] {}", request.getMethod(), requestURL, ex.toString());
return ResultVO.fail(ex.toString());
}
// 兜底处理:Throwable
@ExceptionHandler(value = Throwable.class)
public ResultVO handleThrowable(HttpServletRequest request, Throwable throwable) {
// String requestURL = getUrl(request);
log.error("[{}] {} ", request.getMethod(), "URL地址", throwable);
return ResultVO.fail(AppHttpCodeEnum.FAIL.getMessage());
}
}
在启用全局异常处理功能后,不再需要在接口层手动使用try...catch来处理异常。倘若出现其他异常,它们也会被defaultErrorHandler拦截,从而确保一致地实施统一的返回格式。
四、自动包装类
理论上到这我们已经实现了想要的统一后端响应格式了,但是有没有发现这里存在着一个缺点:每写一个接口都要调用ResultVO来包装返回结果,那是相当的繁琐。
4.1、ResponseBodyAdvice
在SpringBoot中,我们可以利用 ResponseBodyAdvice 来自动包装响应体。ResponseBodyAdvice可以拦截控制器(Controller)方法的返回值,允许我们统一处理返回值或响应体。这对于统一返回格式、加密、签名等场景非常有用。
源码分析:
java
public interface ResponseBodyAdvice<T> {
boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType);
@Nullable
T beforeBodyWrite(@Nullable T body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response);
}
1、supports方法用来判断是否支持advice功能,返回true表示支持,返回false则是不支持
2、beforeBodyWrite则是对返回的数据进行处理。
4.2、定义ResponseResultVO注解
只有标注了这个注解的类或方法,才会对返回结果/响应统一格式。
java
/**
* @Description: TODO:ResponseResult的注解类
* 只有标注了这个注解的类或方法,才会对返回结果/响应统一格式。
* @Author: yyalin
* @CreateDate: 2023/7/16 11:57
* @Version: V1.0
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Documented
public @interface ResponseResultVO {
}
4.3、定义实现类
java
/**
* @Description: TODO:全局响应数据预处理器,使用RestControllerAdvice和ResponseBodyAdvice
* 拦截Controller方法默认返回参数,统一处理响应体
* @Author: yyalin
* @CreateDate: 2023/7/16 12:00
* @Version: V1.0
*/
@RestControllerAdvice
public class RestResponseHandler implements ResponseBodyAdvice<Object> {
// 属性名称,用于记录是否标注了ResponseResultVO注解
public static final String RESPONSE_RESULTVO_ATTR= "RESPONSE_RESULTVO_ATTR";
@Autowired
private ObjectMapper objectMapper;
/**
* 判断是否需要执行beforeBodyWrite方法,true为执行;false为不执行
* @param returnType
* @param converterType
* @return
*/
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
// 判断请求是否有注解标记
ResponseResultVO responseResultVO = (ResponseResultVO) request.getAttribute(RESPONSE_RESULTVO_ATTR);
return responseResultVO != null;
}
/**
* 对返回值包装处理
* @param body
* @param returnType
* @param selectedContentType
* @param selectedConverterType
* @param request
* @param response
* @return
*/
@SneakyThrows
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response) {
// 已经返回的是Result类型对象了,直接返回,比如全局异常处理之后直接返回了
if (body instanceof ResultVO) {
return (ResultVO) body;
} else if (body instanceof String) { // 如果Controller直接返回String时,需要转换为Json,因为强化的是RestController
return objectMapper.writeValueAsString(ResultVO.success(body));
}
return ResultVO.success(body);
}
}
注意:@RestControllerAdvice注解,是对@RestController注解的增强,如果要对@Controller注解增强,那就改为@ControllerAdvice注解即可。supports方法的处理逻辑是查找请求属性中有没有自定义的RESPONSE_RESULT_ATTR,如果有的话就返回true,支持对返回数据包装处理。
4.4、ResponseResultVO拦截器
那这个RESPONSE_RESULTVO_ATTR
是从哪里设置的呢?这其实就是跟上面提到的自定义注解有关了,增加了一个注解,那么必然要知道这个类或方法是否有引用了注解的,才能方便我们后续的操作,因此需要一个自定义拦截器,代码如下:
java
/**
* @Description: TODO:ResponseResultVO拦截器
* @Author: yyalin
* @CreateDate: 2023/7/16 12:11
* @Version: V1.0
*/
public class RestResponseInterceptor implements HandlerInterceptor {
// 属性名称,用于记录是否标注了ResponseResult注解
public static final String RESPONSE_RESULTVO_ATTR= "RESPONSE_RESULTVO_ATTR";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (handler instanceof HandlerMethod) {
final HandlerMethod handlerMethod = (HandlerMethod) handler;
final Class<?> clazz = handlerMethod.getBeanType();
final Method method = handlerMethod.getMethod();
// 判断是否在类对象上添加了注解
if (clazz.isAnnotationPresent(ResponseResultVO.class)) {
// 设置属性,值为该注解
request.setAttribute(RESPONSE_RESULTVO_ATTR, clazz.getAnnotation(ResponseResultVO.class));
} else if (method.isAnnotationPresent(ResponseResultVO.class)){
// 是否在方法上添加了注解
request.setAttribute(RESPONSE_RESULTVO_ATTR, method.getAnnotation(ResponseResultVO.class));
}
}
return true;
}
}
4.5、拦截器注册到Spring中
java
/**
* @Description: TODO:拦截器注册到Spring中
* @Author: yyalin
* @CreateDate: 2023/7/16 12:16
* @Version: V1.0
*/
public class WebConfig implements WebMvcConfigurer {
/**
* 功能描述:SpringMVC 需要手动添加拦截器
* @MethodName: addInterceptors
* @MethodParam: [registry]
* @Return: void
* @Author: yyalin
* @CreateDate: 2023/7/16 12:16
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
//ResponseResultVO拦截器
RestResponseInterceptor respInterceptor = new RestResponseInterceptor();
registry.addInterceptor(respInterceptor);
WebMvcConfigurer.super.addInterceptors(registry);
}
}
五、测试使用
java
/**
* @Description: TODO
* @Author: yyalin
* @CreateDate: 2023/7/16 13:03
* @Version: V1.0
*/
@RestController
@Api(tags="全局统一异常测试")
public class ExceptionController {
@ApiOperation(value="测试", notes="test01")
@PostMapping("/test01")
@ResponseResultVO
public String test01() {
int i = 10 /1;
return "Ok";
}
}