为什么要做全局异常处理
写代码必然会出现异常,Java中程序出错时,通常是以异常的形式展现的:(常见的异常形式)
1.代码不严谨导致的异常,常见的空指针异常
2.系统崩溃、网络抖动或内存溢出等原因导致的异常
只要在写代码--->出现异常--->异常处理
系统设计一般都会做全局异常兜底处理,如果没做处理,默认的异常处理机制会把错误信息全部返回,甚至把一些sql的错误信息打印出来。这样会暴露系统内部的设计,显然是不合理的。
背景
对接的项目多了,奇奇怪怪的问题就都出现了,比如有一个最让人烦心的问题 异常。
偶尔会碰到框架抛出的默认的异常,比如 Laraval,比如 Spring Boot,每个框架抛出的异常格式是不一致的,有 Json 或 XML 格式的数据,当然也有 HTML 页面,最为关键的是响应的数据结构和接口约定的数据结构不一致,所以这时候我们在对响应内容进行解析的时候反而会给我们自己的代码带来需要处理的异常。
基于此,为了对自己的接口负责,我们需要进行全局的异常处理,目的是防止出现约定之外的数据结构。
SpringBoot默认异常处理机制
在SpringBoot中,无论是请求不存在的路径还是@Valid检验不正确或者是业务代码(三层结构中的业务),SpringBoot对错误的默认处理机制:
BasicErrorController会判断当前的请求来自哪里,如果来自浏览器则响应错误页面,如果来自APP则响应JSON。
那么BasicErrorController是如何判断请求类型的呢?其实主要是看HTTP中的一个请求头:Accept
SpringBoot默认异常处理机制有什么值得改善的点呢:
- 样式或数据格式不统一
- 对外暴露的信息不可控
以JSON格式为例,无论接口请求是否正常,都返回统一的JSON格式:
{
"data": {}
"success": true,
"massage": ""
}
如果请求失败,希望把错误信息转化为指定内容(比如“系统正在繁忙”)放在message中返回,给前端、客户端一个友好的提示。
这样一来,不论请求成功还是失败,响应格式都是统一的,对外暴露的信息也是可控的。
自定义异常处理 返回信息可以大致分为两类:
- 自定义错误页面
- 自定义异常JSON
- 自定义错误页面
在resource/errors下存放404.html、500.html,当本次请求的状态码为404或500时,SpringBoot就会读取我们自定义的html页面返回,否则返回默认页面。
现在一般都是前后端分离,所以使用自定义错误页面就略过了。
自定义异常JSON
SpringBoot默认的异常JSON格式:
{
"timestamp": "2021-01-31T01:36:12.187+00:00",
"status": 500,
"error": "Internal Server Error",
"message": "",
"path": "/insertUser"
}
统一结果封装的返回格式:
{
"data": {}
"success": true,
"massage": ""
}
如果前端是使用状态码code判断请求是否正常返回,而不是通过请求的状态判断,当接口异常时,返回的JSON中没有code,就会很错愕,为了统一JSON响应格式,我们需要对异常进行处理。
一般有两种处理方式,并且通常会组合使用:
1.在业务代码中用统一结果封装对象封装
2.使用全局异常处理兜底
通用枚举类、自定义异常:
枚举类:
@Getter
public enum ResponseEnum {
/**
* 通用结果
*/
ERROR(-1, "网络错误"),
SUCCESS(200, "成功"),
/**
* 用户登录
*/
NEED_LOGIN(900, "用户未登录"),
/**
* 参数校验
*/
ERROR_PARAM(10000, "参数错误"),
EMPTY_PARAM(10001, "参数为空"),
ERROR_PARAM_LENGTH(10002, "参数长度错误");
private Integer code;
private String message;
ResponseEnum(Integer code,String message){
this.code = code;
this.message = message;
}
private static Map<Integer,ResponseEnum> ENUM_CACHE;
static {
ENUM_CACHE = Arrays.stream(ResponseEnum.values()).
collect(Collectors.toMap(ResponseEnum::getCode,v->v,(v1,v2) -> v2));
}
public static String getMessage(Integer code){
return Optional.ofNullable(ENUM_CACHE.get(code)).map(v->v.message)
.orElseThrow(() -> new IllegalArgumentException("invalid exception code"));
}
}
自定义异常:
public class BizException extends RuntimeException {
private ResponseEnum responseEnum;
/**
* 构造器,有时我们需要将第三方异常转为自定义异常抛出,但又不想丢失原来的异常信息,此时可以传入cause
* @param responseEnum
* @param cause
*/
BizException(ResponseEnum responseEnum,Throwable cause){
super(cause);
this.responseEnum = responseEnum;
}
/**
* 构造器,通过错误枚举构建自定义异常
* @param responseEnum
*/
BizException(ResponseEnum responseEnum){
this.responseEnum = responseEnum;
}
}
统一结果封装+返回枚举
public class Result<T> {
private Boolean success;
private Integer code;
private String message;
private T data;
Result(Boolean success,Integer code,String message,T data){
this.success = success;
this.code = code;
this.message = message;
this.data = data;
}
/**
* 构造器,不含返回数据
* @param success
* @param message
*/
Result(Boolean success,Integer code,String message){
this(success,code,message,null);
}
/**
* 静态方法,带返回数据
* @param data 统一返回结果封装
* @param <T>
* @return
*/
public static <T> Result<T> success(T data){
return Optional.ofNullable(data).map(value -> new Result<T>(true,ResponseEnum.SUCCESS.getCode(),ResponseEnum.SUCCESS.getMessage(),value))
.orElse(new Result<T>(true,ResponseEnum.SUCCESS.getCode(),ResponseEnum.SUCCESS.getMessage()));
}
/**
* 静态方法,不带返回数据
* @param <T>
* @return
*/
public static <T> Result<T> success(){
return success(null);
}
/**
* 通用错误返回,传入错误枚举
* @param responseEnum
* @param <T>
* @return
*/
public static <T> Result<T> error(ResponseEnum responseEnum){
return new Result<T>(false,responseEnum.getCode(),responseEnum.getMessage());
}
/**
* 通用错误返回,传入错误枚举,支持使用message覆盖
* @param responseEnum
* @param message
* @param <T>
* @return
*/
public static <T> Result<T> error(ResponseEnum responseEnum,String message){
return new Result<T>(false,responseEnum.getCode(),message);
}
/**
* 通用错误返回,只传入message
* @param message
* @param <T>
* @return
*/
public static <T> Result<T> error(String message){
return error(ResponseEnum.ERROR,message);
}
}
定义好了这些通用类之后,我们可以在参数检验时,在业务报异常被捕获时,返回统一的结果封装,最后当有些异常需要统一处理,交由全局异常处理器帮我们封装。
@RestControllerAdvice全局异常处理兜底处理
一般来说,全局异常处理只是一种兜底的异常处理策略,也就是说提倡自己处理异常,但现在其实很多人都喜欢在代码中把异常抛出,全部交由RestContorller处理,所以全局异常处理就非常有必要了,当异常抛到@RestController后,其实还是封装成Result返回了。
所以Result和RestControllerAdvice两种方式归根到底都是统一结果封装后返回。都是返回Result。