Spring Boot从入门到实战:统一异常处理

栏目: Java · 发布时间: 6年前

内容简介:都说管理的精髓就是“制度管人,流程管事”。而所谓流程,就是对一些日常工作环节、方式方法、次序等进行标准化、规范化。且不论精不精髓,在技术团队中,对一些通用场景,统一规范是必要的,只有步调一致,才能高效向前。如前后端交互协议,如本文探讨的异常处理。在spring mvc中,跟异常处理的相关类大致如下

都说管理的精髓就是“制度管人,流程管事”。而所谓流程,就是对一些日常工作环节、方式方法、次序等进行标准化、规范化。且不论精不精髓,在技术团队中,对一些通用场景,统一规范是必要的,只有步调一致,才能高效向前。如前后端交互协议,如本文探讨的异常处理。

1. Spring Mvc中的异常处理

在spring mvc中,跟异常处理的相关类大致如下

Spring Boot从入门到实战:统一异常处理

上图中,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());
    }
}

这里有几点:

  1. 定义了一个includeStackTrace变量,来控制是否输出异常栈信息
  2. 自定义了一个异常类BizException,表示可预知的业务异常,并对它提供了处理方法,见handleBizException方法
  3. 对其它未预知异常,用Exception类型进行最后处理,见handleException方法
  4. 重写了超类的handleExceptionInternal方法,统一响应内容的字段与格式
  5. 针对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的异常处理规范

  1. 异常的表示形式

    异常一般可通过自定义异常类,或定义异常的信息,比如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;
    }
}
  1. 封装异常处理

    分场景定义了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返回客户端。服务端引起的异常调用该方法,如调用第三方服务异常,数据库访问出错等。

在实际使用时,分两种情况,

  1. 不通过try/catch主动抛出异常,如:

    if (StringUtils.isEmpty(appId)) {
        LOG.warn("the authorizer for site[{}] is not existed.", templateMsgRequestDto.getSiteId());
        ExceptionUtil.rethrowClientSideException(ErrorCodeEnum.authorizer_notexist);
    }
    
  2. 通过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


以上所述就是小编给大家介绍的《Spring Boot从入门到实战:统一异常处理》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Domain-Driven Design

Domain-Driven Design

Eric Evans / Addison-Wesley Professional / 2003-8-30 / USD 74.99

"Eric Evans has written a fantastic book on how you can make the design of your software match your mental model of the problem domain you are addressing. "His book is very compatible with XP. It is n......一起来看看 《Domain-Driven Design》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具