本文用到全部源码,见文末
为什么要有全局异常处理
一个软件系统总是会遇到各种异常,包括人为抛出的业务异常,以及bug导致的异常。那怎么对这些异常进行处理呢?
最简单的做法是在controller
层用try/catch
捕获这些异常,然后进行对应的处理.显而易见这种处理方法存在很大的问题,主要是:
-
代码冗余:
controller
中会存在大量的try/catch
代码,这些代码可能内容基本都是一样的,属于垃圾代码 -
不便于修改:假设需要对某种错误类型进行特殊处理,那么需要修改所有的接口,很麻烦,也容易出错
Spring统一异常处理
那么有没有一种方法能够统一处理呢?当然是有的,spring中的AOP就是用来做这样的统一处理的。当然不需要我们来实现这个AOP逻辑。spring已经帮我们实现了。通过@ControllerAdvice
(从英文名称就能看出来这个注解用于处理controller的各种事件通知)注解,可以配合@ExceptionHandler
、@InitBinder
、@ModelAttribute
等注解配套使用.既然是异常处理,那么我们需要用到的就是@ExceptionHandler
.
最简单的用法如下:
建立一个ExceptionHandle类即可(@RestControllerAdvice
就是@ControllerAdvice
和@ResponseBody
的组合注解):
/**
* @author fanxb
* @date 2021-09-24-下午4:37
*/
@RestControllerAdvice
@Slf4j
public class ExceptionHandle {
@ExceptionHandler(Exception.class)
public Result handleException(Exception e) {
log.error("捕获到错误:{}", e.getMessage(), e);
return new Result(0, e.getMessage(), null);
}
}
建立上述类后,controller层抛出的异常全部会进入到handleException
方法中进行统一处理。
注意,只有controller抛出的异常会到这里来,过滤器、拦截器的异常是不行的,因为这里的AOP切点是controller
如何优雅的进行错误处理
上一节说明了如何对异常进行捕获,那么在真实的项目中是如何进行处理的呢?这里介绍一种比较优雅的处理方式。
定义统一返回类
统一接口返回的数据格式,便于前后端交互,同时也方便进行一些统一的操作。
/**
* 下面三个注解是lombok的减负注解,减少一些结构性的编码
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Result implements Serializable {
private static final long serialVersionUID = 451834802206432869L;
/**
* 1:成功,其他失败
*/
private int code;
/**
* message提示内容,当接口异常时提示异常信息
*/
private String message;
/**
* 携带真正需要返回的数据
*/
private Object data;
public static Result success(Object obj) {
return new Result(1, null, obj);
}
}
定义异常类
首先定义一个基础异常类,继承RuntimeException
@Getter
public class BaseException extends RuntimeException {
private static final long serialVersionUID = -3149747381632839680L;
/**
* 基本错误code为0
*/
private static final int BASE_CODE = 0;
private final int code;
private final String message;
public BaseException() {
this(null, null, null);
}
public BaseException(String message) {
this(message, null, null);
}
public BaseException(Exception e) {
this(null, null, e);
}
public BaseException(String message, Exception e) {
this(message, null, e);
}
protected BaseException(String message, Integer code, Exception e) {
super(e);
this.message = message == null ? "" : message;
this.code = code == null ? BASE_CODE : code;
}
@Override
public String getMessage() {
if (this.message != null && this.message.length() > 0) {
return this.message;
}
return super.getMessage();
}
}
可以看到其中有个构造方法是protect
,是为了避免开发图省事直接传入错误码code(强制建立新的业务异常类)
然后如果有自定义的异常,需要建立新的异常类继承BaseException
,比如定义一个CustomBusinessException
public class CustomBusinessException extends BaseException {
private static final long serialVersionUID = 1564935267302330109L;
/**
* 自定义业务异常错误码
*/
private static final int ERROR_CODE = -1;
public CustomBusinessException() {
super("自定义义务异常", ERROR_CODE, null);
}
public CustomBusinessException(String message, Exception e) {
super(message, ERROR_CODE, e);
}
}
之所以用这种新建业务异常类的方式来定义错误码,一方面是为了提高代码可读性,另一方面也是为了方便对异常进行分类处理。
另外也可采用另外一种方式,将错误码定义为枚举类型,构造函数中传入枚举。
编写统一异常处理类
@RestControllerAdvice
@Slf4j
public class ExceptionHandle {
@ExceptionHandler(Exception.class)
public Result handleException(Exception e) {
BaseException be;
if (e instanceof BaseException) {
be = (BaseException) e;
//手动抛出的异常,仅记录message
log.info("baseException:{}", be.getMessage());
if (be instanceof CustomBusinessException) {
//可在这里进行一些针对特定异常的处理逻辑
log.info("customBusinessException:{}", be.getMessage());
}
} else {
//其它异常,非自动抛出的,无需给前端返回具体错误内容(用户不需要看见空指针之类的异常信息)
log.error("other exception:{}", e.getMessage(), e);
be = new BaseException("系统异常,请稍后重试", e);
}
return new Result(be.getCode(), be.getMessage(), null);
}
}
可以将各种统一处理逻辑定义在这里。本类中是使用一个方法来对所有的异常进行处理,在这个方法中再对异常类型进行区分。还有另外一种写法是定义多个handle方法,每个handle处理一种异常。
另外在此还可以统一处理参数校验的异常,无需在接口代码中手动判断,下一篇中专门说明
如何使用
通常代码会分为controller,service,dao三层,业务代码会写在service层中,因此一般是在service层中抛出异常。当然这三层中抛出的未捕获异常都能被Exceptionhandle
捕获。
本文所用到代码见:github