都说管理的精髓就是“制度管人,流程管事”。而所谓流程,就是对一些日常工作环节、方式方法、次序等进行标准化、规范化。且不论精不精髓,在技术团队中,对一些通用场景,统一规范是必要的,只有步调一致,才能高效向前。如前后端交互协议,如本文探讨的异常处理。
在spring mvc中,跟异常处理的相关类大致如下
上图中,spring mvc中处理异常的类(包括在请求映射时与请求处理过程中抛出的异常),都是 HandlerExceptionResolver 接口的实现,并且都实现了 Ordered 接口。与拦截器链类似,如果容器中存在多个实现了 HandlerExceptionResolver 接口的异常处理类,则它们的 resolveException 方法会被依次调用,顺序由order决定,值越小的先执行,只要其中一个调用返回不是null,则后续的异常处理将不再执行。
各实现类简单介绍如下:
因为本文主要是对spring boot如何对异常统一处理进行探讨,所以以上只对各实现做了基本介绍,更加详细的内容可查阅相关文档或后续再补上。
通过第一部分介绍,可以使用@ExceptionHandler + @ControllerAdvice 组合的方式来实现异常的全局统一处理。对于REST服务来说,spring mvc提供了一个抽象类 ResponseEntityExceptionHandler, 该类类似于上面介绍的 DefaultHandlerExceptionResolver,对一些标准的异常进行了处理,但不是返回 ModelAndView对象, 而是返回 ResponseEntity对象。故我们可以基于该类来实现REST服务异常的统一处理
定义异常处理类 BaseWebApplicationExceptionHandler 如下:
@RestControllerAdvice public class BaseWebApplicationExceptionHandler extends ResponseEntityExceptionHandler { private boolean includeStackTrace; public BaseWebApplicationExceptionHandler(boolean includeStackTrace){ super(); this.includeStackTrace = includeStackTrace; } private final Logger logger = LoggerFactory.getLogger(getClass()); @ExceptionHandler(BizException.class) public ResponseEntity<Object> handleBizException(BizException ex) { logger.warn("catch biz exception: " + ex.toString(), ex.getCause()); return this.asResponseEntity(HttpStatus.valueOf(ex.getHttpStatus()), ex.getErrorCode(), ex.getErrorMessage(), ex); } @ExceptionHandler({IllegalArgumentException.class, IllegalStateException.class}) public ResponseEntity<Object> handleIllegalArgumentException(Exception ex) { logger.warn("catch illegal exception.", ex); return this.asResponseEntity(HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST.name().toLowerCase(), ex.getMessage(), ex); } @ExceptionHandler(Exception.class) public ResponseEntity<Object> handleException(Exception ex) { logger.error("catch exception.", ex); return this.asResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR, HttpStatus.INTERNAL_SERVER_ERROR.name().toLowerCase(), ExceptionConstants.INNER_SERVER_ERROR_MSG, ex); } protected ResponseEntity<Object> handleExceptionInternal( Exception ex, @Nullable Object body, HttpHeaders headers, HttpStatus status, WebRequest request) { if (HttpStatus.INTERNAL_SERVER_ERROR.equals(status)) { request.setAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE, ex, WebRequest.SCOPE_REQUEST); } logger.warn("catch uncustom exception.", ex); return this.asResponseEntity(status, status.name().toLowerCase(), ex.getMessage(), ex); } protected ResponseEntity<Object> asResponseEntity(HttpStatus status, String errorCode, String errorMessage, Exception ex) { Map<String, Object> data = new LinkedHashMap<>(); data.put(BizException.ERROR_CODE, errorCode); data.put(BizException.ERROR_MESSAGE, errorMessage); //是否包含异常的stack trace if(includeStackTrace){ addStackTrace(data, ex); } return new ResponseEntity<>(data, status); } private void addStackTrace(Map<String, Object> errorAttributes, Throwable error) { StringWriter stackTrace = new StringWriter(); error.printStackTrace(new PrintWriter(stackTrace)); stackTrace.flush(); errorAttributes.put(BizException.ERROR_TRACE, stackTrace.toString()); } }
这里有几点:
BaseWebApplicationExceptionHandler是通过增强的方式对controller抛出的异常做了统一处理,那如果请求都没有到达controller怎么办,比如在过滤器那边就抛异常了,Spring Boot其实对错误的处理做了一些自动化配置,参考ErrorMvcAutoConfiguration类,具体这里不详述,只提出方案——自定义ErrorAttributes实现,如下所示
public class BaseErrorAttributes extends DefaultErrorAttributes { private boolean includeStackTrace; @Override public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) { Map<String, Object> errorAttributes = new LinkedHashMap<String, Object>(); addStatus(errorAttributes, webRequest); addErrorDetails(errorAttributes, webRequest, this.includeStackTrace); return errorAttributes; }
以上只列出了主要部分,具体实现可参考源码。这里同样定义了includeStackTrace来控制是否包含异常栈信息。
最后,将以上两个实现通过配置文件注入容器,如下:
@Configuration @ConditionalOnClass({Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class}) @ConditionalOnMissingBean(ResponseEntityExceptionHandler.class) @AutoConfigureBefore(ErrorMvcAutoConfiguration.class) public class ExceptionHandlerAutoConfiguration { @Profile({"test", "formal", "prod"}) @Bean public ResponseEntityExceptionHandler defaultGlobalExceptionHandler() { //测试、正式环境,不输出异常的stack trace return new BaseWebApplicationExceptionHandler(false); } @Profile({"default","local","dev"}) @Bean public ResponseEntityExceptionHandler devGlobalExceptionHandler() { //本地、开发环境,输出异常的stack trace return new BaseWebApplicationExceptionHandler(true); } @Profile({"test", "formal", "prod"}) @Bean public ErrorAttributes basicErrorAttributes() { //测试、正式环境,不输出异常的stack trace return new BaseErrorAttributes(false); } @Profile({"default","local","dev"}) @Bean public ErrorAttributes devBasicErrorAttributes() { //本地、开发环境,输出异常的stack trace return new BaseErrorAttributes(true); } }
上面的@Profile主要是控制针对不同环境,输出不同的响应内容。以上配置的意思是在profile为default、local、dev时,响应内容中包含异常栈信息;profile为test、formal、prod时,响应内容不包含异常栈信息。这么做的好处是,开发阶段,当前端联调时,如果出错,可直接从响应内容中看到异常栈,方便服务端开发人员快速定位问题,而测试、生产环境, 就不要返回异常栈信息了。
异常的表示形式
异常一般可通过自定义异常类,或定义异常的信息,比如code,message之类,然后通过一个统一的异常类进行封装。如果每一种异常都定义一个异常类,则会造成异常类过多,所以实践开发中我一般倾向于后者。
可以定义一个接口,该接口主要是方便后面的异常处理工具类实现
public interface BaseErrors { String getCode(); String getMsg(); }
然后定义一个枚举,实现该接口,在该枚举中定义异常信息,如
public enum ErrorCodeEnum implements BaseErrors { qrcode_existed("该公众号下已存在同名二维码"), authorizer_notexist("公众号不存在"), private String msg; private ErrorCodeEnum(String msg) { this.msg = msg; } public String getCode() { return name(); } public String getMsg() { return msg; } }
封装异常处理
分场景定义了ClientSideException,ServerSideException,UnauthorizedException,ForbiddenException异常,分别表示客户端异常(400),服务端异常(500),未授权异常(401),禁止访问异常(403),如ClientSideException定义
public class ClientSideException extends BizException { public <E extends Enum<E> & BaseErrors> ClientSideException(E exceptionCode, Throwable cause) { super(HttpStatus.BAD_REQUEST, exceptionCode, cause); } public <E extends Enum<E> & BaseErrors> ClientSideException(E exceptionCode) { super(HttpStatus.BAD_REQUEST, exceptionCode, null); } }
并且提供一个异常工具类ExceptionUtil,方便不同场景使用,
在实际使用时,分两种情况,
不通过try/catch主动抛出异常,如:
if (StringUtils.isEmpty(appId)) { LOG.warn("the authorizer for site[{}] is not existed.", templateMsgRequestDto.getSiteId()); ExceptionUtil.rethrowClientSideException(ErrorCodeEnum.authorizer_notexist); }
通过try/catch异常重新抛出(注意:可预知的异常,需要给客户端返回某种提示信息的,必须通过该方式重新抛出。否则将返回统一的code 500,提示“抱歉,服务出错了,请稍后重试”的提示信息)如:
try { String result = wxOpenService.getWxOpenComponentService().getWxMpServiceByAppid(appId).getTemplateMsgService().sendTemplateMsg(templateMessage); LOG.info("result: {}", result); } catch (WxErrorException wxException) { //这里不需要打日志,会统一在异常处理里记录日志 ExceptionUtil.rethrowServerSideException(ExceptionCodeEnum.templatemsg_fail, wxException); }
具体实现参考源码: https://github.com/ronwxy/base-spring-boot/tree/master/spring-boot-autoconfigure/src/main/java/cn/jboost/springboot/autoconfig/error
另附demo源码: https://github.com/ronwxy/springboot-demos/tree/master/springboot-error