当我们在开发一个项目时,往往需要对异常进行捕获处理,以提供友好的信息展示给用户。但随着业务的增长,项目越来越复杂,需要捕获异常的地方就会越来越多,如果每个地方都进行try catch,那代码将会变得非常冗余且不好维护。
我们知道Spring Boot默认情况下会映射到 /error 进行异常处理,但是提示并不十分友好。那有没有一种统一的处理机制?幸好从Spring 3.2以后新增了@ControllerAdvice 注解,使用AOP对Controller控制器进行增强(前置增强、后置增强、环绕增强),那么我们就可以对控制器的方法进行调用前(前置增强)和调用后(后置增强)的处理。
Spring还提供了@ExceptionHandler异常增强注解,一般需要配合@RequestBody注解使用。程序如果在执行控制器方法前或执行时抛出异常,会被@ExceptionHandler注解了的方法处理。如果全部异常处理返回json格式,那么可以使用@RestControllerAdvice 代替@ControllerAdvice,这样在方法上就可以不需要添加@ResponseBody。
自定义异常类
定义一个AgException作为全局的自定义异常。继承RuntimeException(运行时异常)对代码无可侵入性,不需要方法中强制捕获或者抛出。
@Getter
@Slf4j
public class AgException extends RuntimeException {
/**
* 响应状态码枚举
*/
private StatusResultEnum statusResult;
private Object[] args;
/**
* 构造指定异常代码与消息参数的业务异常。
*
* @param statusResult 异常代码
* @param args 消息参数,该参数将用于格式化异常代码中的消息字符串
*/
public AgException(StatusResultEnum statusResult, Object... args) {
this(statusResult, null, args);
}
/**
* 构造指定异常代码、异常原因与消息参数的业务异常。
*
* @param statusResult 异常代码
* @param cause 异常消息
* @param args 消息参数,该参数将用于格式化异常代码中的消息字符串
*/
public AgException(StatusResultEnum statusResult, Throwable cause, Object... args) {
super(statusResult.getCodeMsg(args), cause);
log.error("系统异常:{} ", cause.getMessage(), cause);
this.args = args;
this.statusResult = statusResult;
}
}
添加自定义信息枚举类
public enum StatusResultEnum {
SUCCESS("2000", "success", "请求成功"),
/**
* 可预知异常
*/
NOT_LOGIN_IN("4001", "未登录", "未登录"),
/**
* 不可预知异常,但有明确错误码
*/
UN_AUTHORIZED("4002", "权限不足", "权限不足"),
/**
* 不可预知异常,默认错误
*/
INTERNAL_SERVER_ERROR("5000", "%1s", "内部服务器错误,请联系客服人员。");
/**
* 响应返回码
*/
@Getter
private String code;
/**
* 响应描述,面向开发者
*/
@Setter
private String codeMsg;
/**
* 响应描述,面向用户
*/
@Setter
private String statusMsg;
StatusResultEnum(String code, String codeMsg, String statusMsg) {
this.code = code;
this.codeMsg = codeMsg;
this.statusMsg = statusMsg;
}
/**
* 根据指定的占位符参数格式化异常消息。
*
* @param args 占位符参数
* @return 格式化后的异常消息
*/
public String getCodeMsg(Object... args) {
return ErrorCodeUtils.formatMessage(codeMsg, args);
}
/**
* 根据指定的占位符参数格式化异常消息。
*
* @param args 占位符参数
* @return 格式化后的异常消息
*/
public String getStatusMsg(Object... args) {
return ErrorCodeUtils.formatMessage(statusMsg, args);
}
}
全局异常处理类
- 对异常进行归类:可预知异常(即自定义异常AgException);不可预知异常,但需要明确定义错误码;不可预知异常,不需特殊处理错误码。
- 使用Spring MVC控制器增强,捕获全局异常。
- 捕获AgException业务异常,取出错误码和信息构造响应。
- 使用一个线程安全、并且不可更改的map存储不可预知异常自定义的错误信息。
- 捕获AgException以外的异常(Exception),判断map是否定义了该异常错误信息,若有定义取出错误信息构造响应,否则返回默认错误。
@RestControllerAdvice
@Slf4j
public class AgExceptionHandler {
/**
* 线程安全
*/
private static final ImmutableMap<Class<? extends Throwable>, StatusResultEnum> EXCEPTIONS;
static {
final ImmutableMap.Builder<Class<? extends Throwable>, StatusResultEnum> builder = ImmutableMap.builder();
builder.put(LockedAccountException.class, StatusResultEnum.IDENTITY_AUTH_FAIL);
builder.put(UnknownAccountException.class, StatusResultEnum.IDENTITY_AUTH_FAIL);
builder.put(IncorrectCredentialsException.class, StatusResultEnum.IDENTITY_AUTH_FAIL);
builder.put(DisabledAccountException.class, StatusResultEnum.IDENTITY_AUTH_FAIL);
builder.put(UnauthorizedException.class, StatusResultEnum.UN_AUTHORIZED);
builder.put(MissingServletRequestParameterException.class, StatusResultEnum.REQUIRE_ARGUMENT);
// 其他未被发现的异常
builder.put(Exception.class, StatusResultEnum.INTERNAL_SERVER_ERROR);
EXCEPTIONS = builder.build();
}
@ExceptionHandler(AgException.class)
public BaseResponse handleAgException(Throwable e) {
AgException agException = (AgException) e;
return new ResultResponse(agException.getStatusResult(), agException.getArgs());
}
@ExceptionHandler(Exception.class)
public BaseResponse handleException(Exception e) {
log.error("系统异常:{} ", e.getMessage(), e);
StatusResultEnum statusResultEnum = EXCEPTIONS.get(e.getClass());
return new ResultResponse(statusResultEnum, e.getMessage());
}
}
总结
异常抛出的顺序为Dao—Service—Controller—AgExceptionHandler,SpringMVC增强的即是在Controller层进行拦截,实现全局异常统捕获,异常在AgExceptionHandler 统一处理后,就无需再代码中单独对每个服务进行try catch,此种实现方式代码不仅重用性高,而易于扩展。
参考