当我们在开发一个项目时,往往需要对异常进行捕获处理,以提供友好的信息展示给用户。但随着业务的增长,项目越来越复杂,需要捕获异常的地方就会越来越多,如果每个地方都进行try catch,那代码将会变得非常冗余且不好维护。
我们知道Spring Boot默认情况下会映射到 /error 进行异常处理,但是提示并不十分友好。那有没有一种统一的处理机制?幸好从Spring 3.2以后新增了@ControllerAdvice 注解,使用AOP对Controller控制器进行增强(前置增强、后置增强、环绕增强),那么我们就可以对控制器的方法进行调用前(前置增强)和调用后(后置增强)的处理。
Spring还提供了@ExceptionHandler异常增强注解,一般需要配合@RequestBody注解使用。程序如果在执行控制器方法前或执行时抛出异常,会被@ExceptionHandler注解了的方法处理。如果全部异常处理返回json格式,那么可以使用@RestControllerAdvice 代替@ControllerAdvice,这样在方法上就可以不需要添加@ResponseBody。
自定义异常类
定义一个AgException作为全局的自定义异常。继承RuntimeException(运行时异常)对代码无可侵入性,不需要方法中强制捕获或者抛出。
@Getter@Slf4jpublic 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@Slf4jpublic class AgExceptionHandler { /** * 线程安全 */ private static final ImmutableMap, StatusResultEnum> EXCEPTIONS; static { final ImmutableMap.Builder , 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,此种实现方式代码不仅重用性高,而易于扩展。
参考