在实际开发中,异常永远是绕不过的一个话题,那么如何优雅地处理异常是一个非常值得思考的问题。我们都知道SpringBoot中有一个@RestControllerAdvice注解,通过它和@ExceptionHandler搭配使用,可以对全局异常进行捕获,然后进行封装返回。
那么自然而然的,能够想到的第一种处理方案就是在业务的处理过程中,直接抛出Exception异常,或者RuntimeException,然后直接对全局的Exception进行捕获,进行简单的封装返回,比如:
@RestControllerAdvice
public class GlobalExceptionAdvice {
/**
* 异常处理器
*
* @param exception 异常信息
* @return Response
*/
@ExceptionHandler(value = Exception.class)
@ResponseStatus(code = HttpStatus.INTERNAL_SERVER_ERROR)
public Response exceptionHandler(Exception exception) {
return new Response(500, "服务系统异常: " + exception.getMessage());
}
}
不过这种实现方式过于简单粗暴,直接用一个Exception解决全部,当真正抛出异常之后,非常不利于我们去排查问题,所以这种方式基本不会列入考虑范围。
我们之所以要对异常进行捕获,有几方面的原因,第一个就是当前端调用后端的接口时能够清晰地反应出调用结果,是调用成功还是调用失败,如果是调用失败,具体的原因又是什么,是由于参数传递错误呢,还是由于参数类型错误呢等等;第二个就是当出现问题的时候能够快速地帮助我们定位问题。
既然如此,我们可以在第一种处理方式上进行改进,自定义一个业务异常,比如CustomException,为了方便我们对问题进行排查,在异常中我们可以定义两个字段,一个是code,一个是message,也就是异常信息,比如下面这样:
public class CustomException extends RuntimeException {
private final Integer code;
private final String message;
public CustomException(Integer code, String message) {
this.code = code;
this.message = message;
}
public Integer getCode() {
return code;
}
@Override
public String getMessage() {
return message;
}
}
然后,我们可以在全局异常处理中对CustomException进行处理,然后封装返回,这种方式无疑会比上一种方式好很多,我们可以对每一种错误信息进行归类,每一个code码都对应一种异常,区分地越细,当出现问题的时候就越方便我们进行排查。
在上面的实现中,CustomException的构造方法包含了code和message两个参数,我们要使用的话就是throw CustomException(1001, "设备不存在"),如果很多地方都存在设备不存在这个错误的话,就要重复书写很多次设备不存在,考虑到代码的复用性,一般我们不会这么设计,而是只传递一个code,code和message之间的映射会单独地写到一个配置文件中,使用的时候就是throw CustomException(1001)了。
public class ExceptionMessage {
private static final Map<Integer, String> exceptionMap = new HashMap<>();
static {
exceptionMap.put(1001, "设备不存在");
exceptionMap.put(2001, "禁止访问");
exceptionMap.put(2002, "权限不足");
}
public static String getMessage(Integer code) {
return exceptionMap.get(code);
}
}
public class CustomException extends RuntimeException {
private final Integer code;
private final String message;
public CustomException(Integer code, String message) {
this.code = code;
this.message = message;
}
public CustomException(Integer code) {
this.code = code;
this.message = ExceptionMessage.getMessage(code);
}
public Integer getCode() {
return code;
}
@Override
public String getMessage() {
return message;
}
}
这样我们在使用的还是就是throw CustomException(1001),也可以是throw CustomException(1001, "设备不存在"),两种方式都可以,但是这种方式其实也有一个小问题就是1001这种纯数字的code码,可读性也不是很好,一旦错误类型分得越细,这些code码我们是记不住的,需要经常性地去查看code和message之间的映射。
既然如此,我们可以尝试使用枚举来给每一种错误都起一个名字,这样的话可以解决可读性的问题。
public enum ExceptionEnum {
DEVICE_NOT_EXIST(1001, "设备不存在"),
FORBIDEEN(2001, "进制访问"),
PERMISSION_NOT_ENOUGH(2002, "权限不足");
private final Integer code;
private final String message;
ExceptionEnum(Integer code, String message) {
this.code = code;
this.message = message;
}
public Integer getCode() {
return this.code;
}
public String getMessage() {
return this.message;
}
}
public class CustomException extends RuntimeException {
private final Integer code;
private final String message;
public CustomException(ExceptionEnum exceptionEnum) {
this.code = exceptionEnum.getCode();
this.message = exceptionEnum.getMessage();
}
public Integer getCode() {
return code;
}
@Override
public String getMessage() {
return message;
}
}
这样的话,我们在使用的时候就是throw new CustomException(ExceptionEnum.PERMISSION_NOT_ENOUGH),可读性倒是比之前好了很多,但是也会有一个小问题,就是如果错误划分地越细的话,我们给这些错误起名字的时候可能就会犯难了,所以到底选择使用枚举来表示一种错误类型,还是使用一个code码来表示一种错误类型,需要靠自己进行抉择。
第二种设计方式无疑比第一种设计方式要好了很多,对异常信息进行了细化,方便我们去排查问题,但是还是有两个问题仍待我们去解决,第一个问题就是在响应中http状态码该如何进行返回,我们都知道一个http请求的响应状态码有很多,比如200表示成功,403表示禁止访问等等,那么我们处理的时候是不是也可以按照这个标准来进行返回呢?第二个问题就是代码的可读性,在代码的任何需要抛异常的地方都抛出CustomException的话,可读性并不是很高,我们是不是可以进一步提高代码的可读性呢?
基于上述两个问题,我们就需要对异常类进行完善,具体可以参照http状态码,可以定义一个基础的HttpException。
public class HttpException extends RuntimeException {
protected String message;
protected Integer code;
protected Integer httpStatusCode = 500;
public Integer getCode() {
return code;
}
public Integer getHttpStatusCode() {
return httpStatusCode;
}
@Override
public String getMessage() {
return message;
}
}
在这个类中,code和message和原来的CustomException含义是一致的,多了一个字段httpStatusCode用来标记返回给前端的http状态码,默认是500。
基于这个HttpException,我们可以扩展出NotFoundException、ForbiddenException、ParameterExceptioin、DuplicateException等等,对异常类进一步划分。当某一个方法被禁止访问的时候,我们就可以抛出ForbiddenException,当某一个方法的传参出错的时候,我们就可以抛出ParameterException等等。
public class NotFoundException extends HttpException {
public NotFoundException(int code) {
this.code = code;
this.httpStatusCode = 404;
}
}
同时,在全局异常处理中,我们可以只需要对HttpException进行捕获处理即可,因为这些异常都是对HttpException的扩展,同时,还可以从异常类中获取到当前异常对应的http状态码,进行返回。
@RestControllerAdvice
public class GlobalExceptionAdvice {
/**
* 异常处理器
*
* @param exception 异常信息
* @return Response
*/
@ExceptionHandler(value = Exception.class)
@ResponseStatus(code = HttpStatus.INTERNAL_SERVER_ERROR)
public Response exceptionHandler(Exception exception) {
return new Response(500, "服务系统异常: " + exception.getMessage());
}
/**
* HTTP类型异常处理器
*
* @param exception 异常信息
* @return 异常封装之后返回的Response
*/
@ExceptionHandler(HttpException.class)
public ResponseEntity<Response> httpExceptionHandler(HttpException exception) {
Integer code = exception.getCode();
String message = exceptionCodeConfiguration.getMessage(code);
Response response = new Response(code, message);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpStatus httpStatus = HttpStatus.resolve(exception.getHttpStatusCode());
if (httpStatus == null) {
httpStatus = HttpStatus.INTERNAL_SERVER_ERROR;
}
return new ResponseEntity<>(response, headers, httpStatus);
}
}
这样的话,我们就在原来的基础上对异常再次进行了细化,而且还添加上了http状态码了,另外使用code码来表示错误信息还有另外一个好处,就是前端后端都可以共用一份code码,后端在接口返回中,每一种code码对应的异常信息有的时候可能会偏向于技术一点,如果前端直接拿着这个异常信息返回给用户的时候,有的时候是不友好的,因为可能用户看不懂,那么前端就也可以维护一份code和错误信息之间的映射,来返回给用户。
比如下面这样:
# 后端
{
1001: "设备不存在",
2001: "禁止访问",
2002: "权限不足",
3001: "商品服务调用失败",
}
# 前端
{
1001: "你说啥",
2001: "说的啥",
2002: "哈哈哈哈",
3001: "服务错误"
}