目录
前言
全局响应
数据规范
状态码(错误码)
全局响应类
使用
优化
全局异常处理
为什么需要全局异常处理
业务异常类
全局捕获
使用
优化
总结
前言
在悦享校园1.0版本中的数据返回采用了以Map对象返回的方式,虽然较为便捷但也带来一些问题。一是在Controller中所有方法均需要实例化一个Map对象。二是当返回数据较多时使用put方式添加信息会容易出现遗漏的问题。在异常处理方面,虽然该版本中对所有异常通过继承RuntimeException的方式来进行封装,但业务异常较多时这一操作就显得冗余,且需要使用上述提到的Map对象包装异常信息。对于以上问题在2.0版本中通过结合SpringBoot来进行优雅的解决。
全局响应
数据规范
一般来讲我们提供给前端接口调用的返回值为如下的JSON格式,其包含结果状态,状态码,响应信息和响应数据。通常会使用@ResponseBody注解配合一个响应类来实现这一功能。但需要注意的是,当方法返回值为String类型时,@ResponseBody注解并不会将其转为JSON格式,需要手动进行转换。
{
"success": true,
"code": 0,
"message": "操作成功",
"data": "Hello"
}
状态码(错误码)
通过第一步数据规范可知,当接口被调用后会返回对应信息,若调用成功时返回固定的状态码即可,但调用失败时则需要不同的状态码来标识。为解决这个问题这里使用枚举的方式来定义出现异常时的错误信息。(此处的枚举对象名称可以自定义)
块的错误。(此处的枚举对象名称可以自定义)
@AllArgsConstructor
@Getter
public enum ExceptionCodeEnum {
// 操作成功
EC0(0,"操作成功"),
// 通用模块错误
EC10000(10000,"系统内部错误"),
EC10001(10001,"参数错误"),
EC10002(10002,"资源不存在"),
// 用户模块错误
EC20000(20000,"用户名已被占用"),
EC20001(20001,"用户不存在"),
EC20002(20002,"用户名或密码错误"),
// 其它模块....
/**
* 异常代码
*/
private Integer code;
/**
* 描述信息
*/
private String message;
}
全局响应类
此处创建一个泛型类来实现全局返回信息的格式统一,并且提供返回结果不同状态下的构造方法。
@Data
@Builder
@AllArgsConstructor
public class ResultDataVO<T> {
/**
* 调用结果状态
*/
private Boolean success;
/**
* 响应代码
*/
private Integer code;
/**
* 详细信息
*/
private String message;
/**
* 返回数据,数据为空则不返回
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
private T data;
/**
* 操作成功时返回的数据
* @param result
* @param <T>
* @return
*/
public static <T> ResultDataVO<T> success(T result) {
return ResultDataVO.<T>builder()
.success(true)
.code(ExceptionCodeEnum.EC0.getCode())
.message(ExceptionCodeEnum.EC0.getMessage())
.data(result)
.build();
}
/**
* 操作失败
* @param <T>
* @param exceptionCodeEnum 错误类型枚举
* @return
*/
public static <T> ResultDataVO<T> failure(ExceptionCodeEnum exceptionCodeEnum){
return ResultDataVO.<T>builder()
.success(false)
.code(exceptionCodeEnum.getCode())
.message(exceptionCodeEnum.getMessage())
.data(null)
.build();
}
/**
* 操作失败,返回信息
* @param exceptionCodeEnum 错误信息列表
* @param result 对应失败信息对象
* @param <T>
* @return
*/
public static <T> ResultDataVO<T> failure(ExceptionCodeEnum exceptionCodeEnum, T result){
return ResultDataVO.<T>builder()
.success(false)
.code(exceptionCodeEnum.getCode())
.message(exceptionCodeEnum.getMessage())
.data(result)
.build();
}
}
使用
通过以上操作已经实现了一个基础的全局数据响应处理,可以通过如下方式来使用。
@GetMapping("/{id}")
public ResultDataVO getMsg(@RequestParam(required = false) String name,
@Max(value = 10,message = "最大值不能超过10")
@PathVariable(name = "id") int uid) {
String result = "Hello,"+name+" id "+uid;
return ResultDataVO.success(result);
}
优化
虽然到这里我们已经基本实现了全局响应,但如果不想要在每个方法中调用ResultDataVO的success方法,可以通过如下方式解决。这里新建处理类实现了ResponseBodyAdvice接口,该接口包含三个方法,supports、beforeBodyWrite、handleEmptyBody。
supports用于指明方法是否需要对进入的方法进行后续包装处理,默认返回true,即对所有方法处理。
beforeBodyWrite用于在控制器方法返回结果后,但在响应体写入之前调用。可以在此方法中修改body对象,如包装、添加元数据等。在该方法中将使用ResultDataVO的success方法进行包装,由此将可以省去在Controller方法中重复调用success方法。
handleEmptyBody用于处理null值,由于ResultDataVO类中已经对null值进行了处理,因此无需重写该方法。
注意:即使在全异处理添加了@RestControllerAdvice注解后,仍需要在Controller类上添加@ResponseBody注解,或者直接使用@RestController注解。因为方法返回值经过invokeAndHandle处理后已经确定,若没有以上注解则默认会按照String类型进行路径映射查找视图,进而返回404错误,而@RestControllerAdvice的相关方法会在invokeAndHandle处理后再进行调用。
@RestControllerAdvice
@Slf4j
public class GlobalExceptionAdvice implements ResponseBodyAdvice<Object> {
/**
* json格式化操作
*/
@Resource
private ObjectMapper objectMapper;
/**
* 是否开启对所有方法的处理,可以在此方法中添加条件使其支持对特定方法的处理。
*/
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}
/**
* 用于在控制器方法返回结果后,但在响应体写入之前调用。可以此处对数据进行包装等操作
*/
@SneakyThrows
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
/**
* 未被捕获的错误进行拦截
*/
if(body == null){
log.error("未处理的异常信息,请检查错误日志");
return ResultDataVO.failure(ExceptionCodeEnum.EC10000);
}
/**
* 返回类型为String则需要手动序列化
*/
if (body instanceof String) {
return objectMapper.writeValueAsString(ResultDataVO.success(body));
}
/**
* 已被包装为全局VO对象直接返回
*/
if (body instanceof ResultDataVO) {
return body;
}
/**
* 判断是否为404,500等错误类型
*/
if (body instanceof LinkedHashMap) {
LinkedHashMap<String, Object> httpErrorCode = (LinkedHashMap<String, Object>) body;
Integer code = (Integer) httpErrorCode.get("status");
String message = (String) httpErrorCode.get("error");
return new ResultDataVO(false, code, message, null);
}
return ResultDataVO.success(body);
}
}
全局异常处理
为什么需要全局异常处理
使用全局异常处理更加灵活和规范化, 所有错误信息会被封装后返回给前端,避免暴露业务细节。
业务异常类
由于代码在运行过程中会出现异常,通常我们会使用 try...catch 方式来捕获并处理,在此之后我们需要返回错误信息知调用者当前状况。由于我们处理的异常多为RuntimeException的子类,因此可以通过编写一个业务异常类来实现总的异常信息处理,与以往不同在这里并不会为所有的业务异常创建具体的异常类,将使用前文中的错误码来配合使用。
@Getter
public class BusinessException extends RuntimeException {
/**
* 错误对象枚举
*/
private ExceptionCodeEnum codeEnum;
/**
* 根据传入的异常枚举解析异常相关信息。
* @param codeEnum
*/
public BusinessException(ExceptionCodeEnum codeEnum){
this.codeEnum = codeEnum;
}
}
全局捕获
由于已经定义了总的异常处理类,因此在使用时只需要通过抛出 BusinessException 对象即可。但我们需要在代码中写入大量的try-catch语句来捕获处理异常。并且对于错误信息的返回需要符合在全局响应中的数据规范,也就是说需要像全局响应一样统一调用ResultDataVO的failure方法。
在前文创建的GlobalExceptionAdvice类上有一个@RestControllerAdvice注解,该注解将使所有的异常都进入到此处被处理,同时也可以用于全局的数据绑定、格式化等。
既然所有的异常都进入该类处理,那么如何处理呢?这里使用@ExceptionHandler注解,使用它可以指定当前方法处理哪种类型的异常,示例代码如下。通过在方法体内调ResultDataVO的failure方法来完成返回数据格式的规范,这里仅列举了三个异常处理,可自行添加更多的异常类。
/**
* 数据格式转换错误
*/
@ExceptionHandler(DataFormatException.class)
@ResponseBody
public ResultDataVO dataFormatExceptionHandler(DataFormatException e) {
log.error("捕获数据格式转换错误异常", e);
return ResultDataVO.failure(ExceptionCodeEnum.EC10001);
}
/**
* 业务异常捕获
*
* @param businessException
* @return
*/
@ExceptionHandler(value = BusinessException.class)
public ResultDataVO handleBusinessException(BusinessException businessException) {
log.error("捕获业务异常", businessException);
return ResultDataVO.failure(businessException.getCodeEnum());
}
/**
* 系统级异常
*
* @param throwable
*/
@ExceptionHandler(value = Throwable.class)
public ResultDataVO handleThrowable(Throwable throwable) {
log.error("捕获系统级异常", throwable);
return ResultDataVO.failure(ExceptionCodeEnum.EC10000);
}
使用
通过上述操作可以实现一定程度上对try-catch的消除,示例代码如下
/**
* 统一异常处理
* @return
*/
@GetMapping("/error")
public ResultDataVO getError(){
// try...catch
int res = 1 / 0;
return ResultDataVO.success(res);
}
优化
虽然采用上述方式已经实现了对异常的统一拦截处理并返回,但若异常产生的源头并非Crontroller中出现而是在系统内部时则有可能导致返回结果出现问题,该问题已经在GlobalExceptionAdvice类的beforeBodyWrite方法中做了处理,当body对象为空时则仍然会调用ResultDataVO的failure方法。
// 用于处理未被正常捕获的异常
if(body == null){
log.error("未处理的异常信息,请检查错误日志");
return ResultDataVO.failure(ExceptionCodeEnum.EC10000);
}
// ......
总结
通过使用@RestControllerAdvice和@ExceptionHandler注解和错误码以及对应处理/响应类即可实现全局异常的统一处理,其中GlobalExceptionAdvice类对全局响应和异常处理做了合并,可按照业务需求自行拆分。