都说管理的精髓就是 "制度管人, 流程管事". 而所谓流程, 就是对一些日常工作环节, 方式方法, 次序等进行标准化, 规范化. 且不论精不精髓, 在技术团队中, 对一些通用场景, 统一规范是必要的, 只有步调一致, 才能高效向前. 如前后端交互协议, 如本文探讨的异常处理.
1. Spring Mvc 中的异常处理
在 spring mvc 中, 跟异常处理的相关类大致如下
上图中, spring mvc 中处理异常的类(包括在请求映射时与请求处理过程中抛出的异常), 都是 HandlerExceptionResolver 接口的实现, 并且都实现了 Ordered 接口. 与拦截器链类似, 如果容器中存在多个实现了 HandlerExceptionResolver 接口的异常处理类, 则它们的 resolveException 方法会被依次调用, 顺序由 order 决定, 值越小的先执行, 只要其中一个调用返回不是 null, 则后续的异常处理将不再执行.
各实现类简单介绍如下:
DefaultHandlerExceptionResolver: 这个是默认实现, 处理 Spring 定义的各种标准异常, 将其转换为对应的 Http Status Code, 具体处理的异常参考 doResolveException 方法
ResponseStatusExceptionResolver: 用来支持 @ResponseStatus 注解使用的实现, 如果自定义的异常通过 @ResponseStatus 注解进行了修饰, 并且容器中存在 ResponseStatusExceptionResolver 的 bean, 则自定义异常抛出时会被该 bean 进行处理, 返回注解定义的 Http Status Code 及内容给客户端
ExceptionHandlerExceptionResolver: 用来支持 @ExceptionHandler 注解使用的实现, 使用该注解修饰的方法来处理对应的异常. 不过该注解的作用范围只在 controller 类, 如果需要全局处理, 则需要配合 @ControllerAdvice 注解使用.
SimpleMappingExceptionResolver: 将异常映射为视图
HandlerExceptionResolverComposite: 就是各类实现的组合, 依次执行, 只要其中一个处理返回不为 null, 则不再处理.
因为本文主要是对 spring boot 如何对异常统一处理进行探讨, 所以以上只对各实现做了基本介绍, 更加详细的内容可查阅相关文档或后续再补上.
2. 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());
- }
- }
这里有几点:
定义了一个 includeStackTrace 变量, 来控制是否输出异常栈信息
自定义了一个异常类 BizException, 表示可预知的业务异常, 并对它提供了处理方法, 见 handleBizException 方法
对其它未预知异常, 用 Exception 类型进行最后处理, 见 handleException 方法
重写了超类的 handleExceptionInternal 方法, 统一响应内容的字段与格式
针对 REST 服务, 使用的是 @RestControllerAdvice 注解, 而不是 @ControllerAdvice
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 时, 响应内容不包含异常栈信息. 这么做的好处是, 开发阶段, 当前端联调时, 如果出错, 可直接从响应内容中看到异常栈, 方便服务端开发人员快速定位问题, 而测试, 生产环境, 就不要返回异常栈信息了.
3. 基于 Spring Boot 的异常处理规范
异常的表示形式
异常一般可通过自定义异常类, 或定义异常的信息, 比如 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, 方便不同场景使用,
rethrowClientSideException: 抛出 ClientSideException, 将以 status code 400 返回客户端. 由客户端引起的异常调用该方法, 如参数校验失败.
rethrowUnauthorizedException: 抛出 UnauthorizedException, 将以 status code 401 返回客户端. 访问未授权时调用, 如 token 校验失败等.
rethrowForbiddenException: 抛出 ForbidenException, 将以 status code 403 返回客户端. 访问被禁止时调用, 如用户被禁用等.
rethrowServerSideException: 抛出 ServerSideException, 将以 status code 500 返回客户端. 服务端引起的异常调用该方法, 如调用第三方服务异常, 数据库访问出错等.
在实际使用时, 分两种情况,
不通过 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);
- }
具体实现参考源码:
另附 demo 源码:
4. 总结
本文写完感觉信息量有点多, 对于不具备一定基础的人来说理解可能有点难度. 如果有任何疑问, 欢迎交流. 后续有需要的话也可以针对某个环节再进行细化补充. 本文所提的规范不一定是最好的实践, 但规范或流程的管理, 都是遵循先僵化, 后优化, 再固化的步骤, 先解决有没有的问题, 再解决好不好的问题.
我的个人博客地址: http://blog.jboost.cn/
我的 GitHub 地址: https://github.com/ronwxy
我的微信公众号: jboost-ksxy (一个不只有技术干货的公众号, 欢迎关注)
--------------------------------------------------
来源: http://www.bubuko.com/infodetail-3112590.html