笔者这边提供了两种处理全局异常的方式。这两种方式各有千秋,都很优雅。至于伙伴们想用哪种方式,那就仁者见仁,智者见智了。
0、公共部分
在介绍异常处理方式前,先定义一些公共的类。这些类在两种处理方式中都会用到。
【自定义业务异常】
/**
* 自定义业务异常
*/
@Data
public class SunException extends RuntimeException {
private Integer code;
private String msg;
public SunException(SystemEnum systemEnum) {
this.code = systemEnum.getCode();
this.msg = systemEnum.getDesc();
}
public SunException(BusinessEnum businessEnum) {
this.code = businessEnum.getCode();
this.msg = businessEnum.getDesc();
}
public SunException(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
}
【自定义系统枚举】
/**
* 系统枚举
*/
public enum SystemEnum {
SUCCESS(0, "success"),
FAIL(-1, "fail"),
PARAM_ILLEGAL(100, "参数非法!"),
SERVICE_TIME_OUT(200, "服务间调用超时"),
UNEXPECTED_EXCEPTION(500, "系统内部错误,请联系管理员!"),
OTHER(9999, "Unknown Exception.");
private Integer code;
private String desc;
SystemEnum(Integer code, String desc) {
this.code = code;
this.desc = desc;
}
public Integer getCode() {
return code;
}
public String getDesc() {
return desc;
}
}
【自定义业务枚举】
/**
* 业务类枚举
*
* @author: dong
* @date: 2023/2/12 21:50
* @since: 1.0
*/
public enum BusinessEnum {
/**--------用户相关----------**/
USER_ID_NOT_EXIST(1000, "userId not exist."),
/**--------任务相关----------**/
TASK_ID_NOT_EXIST(2000, "taskId not exist."),
TASK_NAME_NOT_EXIST(2001, "taskName not exist."),
TASK_TYPE_NOT_EXIST(2002, "taskType not exist."),
DEADLINE_NOT_EXIST(2003, "deadline not exist."),
CONTENT_NOT_EXIST(2004, "content not exist.")
;
private Integer code;
private String desc;
BusinessEnum(Integer code, String desc) {
this.code = code;
this.desc = desc;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
}
【统一封装响应体】
/**
* 统一封装响应体
* @param <T>
*/
public class BaseResult<T> {
private Integer code;
private String msg;
private T data;
public BaseResult() {
}
public BaseResult(Integer code, String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
}
public BaseResult(T data) {
this.code = SystemEnum.SUCCESS.getCode();
this.msg = SystemEnum.SUCCESS.getDesc();
this.data = data;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
【响应结果工具类】
/**
* 响应结果工具类
*/
public class ResultUtil {
public static <R> BaseResult<R> outSuccess() {
return new BaseResult<>(SystemEnum.SUCCESS.getCode(), SystemEnum.SUCCESS.getDesc(), null);
}
public static <R> BaseResult<R> outSuccess(R data) {
return new BaseResult<>(data);
}
public static <R> BaseResult<R> outFail(String errorMsg) {
return new BaseResult<>(SystemEnum.FAIL.getCode(), errorMsg, null);
}
public static <R> BaseResult<R> outFail(Integer errorCode, String errorMsg) {
return new BaseResult<>(errorCode, errorMsg, null);
}
}
方式一、@RestControllerAdvice + @ExceptionHandler
如下所示,
a. 新建一个全局异常处理类,并在类名前加上@RestControllerAdvice注解,该注解可以拦截项目中抛出的异常;
b. 同时在新建一个处理异常的方法,并在方法上加上@ExceptionHandler注解,并在该注解的属性中指定具体的异常。如下代码中指定的具体异常即是 SunException(也就是上一小节中笔者自定义的业务异常)。
c. 在处理异常的方法中,通过ResultUtil.outFail() 方法统一封装返回给前端的响应体。
/**
* 全局异常处理
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(GlobalExceptionHandler.class);
@ExceptionHandler(SunException.class)
@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
public BaseResult handlerBusinessException(SunException sunException) {
LOGGER.error("exception happened at {}", sunException.getMsg());
return ResultUtil.outFail(sunException.getCode(), sunException.getMsg());
}
}
那么在具体的业务接口中如何抛出异常能被GlobalExceptionHandler所捕获呢?
其实很简单,只要用 throw new SunException(...); 就可以了。注意:这里只能抛出SunException,不能抛出RuntimeException或者任何其他异常。因为@ExceptionHandler已经指定了具体的异常类型。
@GetMapping("/hello")
public String sayHello(String name) {
if (StringUtils.isEmpty(name)) {
throw new SunException(SystemEnum.PARAM_ILLEGAL);
}
return "The sun is rising.";
}
Swagger测试效果:
方式二、AOP实现
很明显,aop天生就是干这个的料。aop在业务解耦方面简直如鱼得水,像统一打印日志,统一捕获异常等等。talk is cheap,show me the code.
如下代码所示:
a. 新建这个aop监控类,新增切点,切所有模块的Controller层的所有方法;
b. 实现一个环绕通知接口,打印方法入参以及请求结果;
c. 注意代码40行到45行,就是捕获项目中所有的SunException,并封装成统一的响应体返回前端。
/**
* aop监控类
**/
@Slf4j
@Aspect
@Component
public class AspectMonitor {
/**
* 日志切点,切所有模块controller层中的所有方法
*/
@Pointcut("execution(* com.bxbro.*..controller..*.*(..))")
public void logPointCut() {
// do nothing.
}
/**
* 对接口做统一的日志及异常处理
* @param pjp
* @return
*/
@Around("logPointCut()")
public Object apiMonitor(ProceedingJoinPoint pjp) {
Object[] args = pjp.getArgs();
Object[] arguments = new Object[args.length];
for (int i=0;i<args.length;i++) {
// ServletRequest不能序列化,从入参里排除,
// 否则报异常:java.lang.IllegalStateException: It is illegal to call this method if the current request is not in asynchronous mode (i.e. isAsyncStarted() returns false)
if(args[i] instanceof ServletRequest || args[i] instanceof ServletResponse || args[i] instanceof MultipartFile) {
continue;
}
arguments[i] = args[i];
}
log.info("请求入参:{},方法名:{}.{}", JSON.toJSONString(arguments),pjp.getSignature().getDeclaringTypeName(),pjp.getSignature().getName());
Object result;
try {
result = pjp.proceed();
} catch (Throwable throwable) {
if (throwable instanceof SunException) {
SunException sunException = (SunException) throwable;
return ResultUtil.outFail(sunException.getCode(), sunException.getMsg());
}
log.error("接口请求异常:{}", throwable);
return ResultUtil.outFail("系统内部错误" + throwable.getMessage());
}
log.info("请求结果:{}", JSON.toJSONString(result));
return result;
}
}