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

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

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());
    }
}

这里有几点:

  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();
    }
  2. 然后定义一个枚举,实现该接口,在该枚举中定义异常信息,如

    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;
        }
    }
  3. 封装异常处理 
    分场景定义了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

4. 总结

本文写完感觉信息量有点多,对于不具备一定基础的人来说理解可能有点难度。如果有任何疑问,欢迎交流。后续有需要的话也可以针对某个环节再进行细化补充。本文所提的规范不一定是最好的实践,但规范或流程的管理,都是遵循先僵化,后优化,再固化的步骤,先解决有没有的问题,再解决好不好的问题。

我的个人博客地址:http://blog.jboost.cn
我的github地址:https://github.com/ronwxy
我的微信公众号:jboost-ksxy (一个不只有技术干货的公众号,欢迎关注)
——————————————————————————————————————————————————

欢迎关注我的微信公众号,及时获取最新分享

原文地址:https://www.cnblogs.com/spec-dog/p/11130199.html

时间: 2024-10-05 06:41:26

Spring Boot2从入门到实战:统一异常处理的相关文章

Spring Boot2 系列教程 (十四) | 统一异常处理

如题,今天介绍 SpringBoot 是如何统一处理全局异常的.SpringBoot 中的全局异常处理主要起作用的两个注解是 @ControllerAdvice 和 @ExceptionHandler ,其中 @ControllerAdvice 是组件注解,添加了这个注解的类能够拦截 Controller 的请求,而 ExceptionHandler 注解可以设置全局处理控制里的异常类型来拦截要处理的异常. 比如:@ExceptionHandler(value = NullPointExcept

spring-boot实战【07】【转】:Spring Boot中Web应用的统一异常处理

我们在做Web应用的时候,请求处理过程中发生错误是非常常见的情况.Spring Boot提供了一个默认的映射:/error,当处理中抛出异常之后,会转到该请求中处理,并且该请求有一个全局的错误页面用来展示异常内容. 选择一个之前实现过的Web应用(Chapter3-1-2)为基础,启动该应用,访问一个不存在的URL,或是修改处理内容,直接抛出异常,如: 1 2 3 4 @RequestMapping("/hello") public String hello() throws Exce

Spring Boot中Web应用的统一异常处理

我们在做Web应用的时候,请求处理过程中发生错误是非常常见的情况.Spring Boot提供了一个默认的映射:/error,当处理中抛出异常之后,会转到该请求中处理,并且该请求有一个全局的错误页面用来展示异常内容. 选择一个之前实现过的Web应用(Chapter3-1-2)为基础,启动该应用,访问一个不存在的URL,或是修改处理内容,直接抛出异常,如: 1 2 3 4 @RequestMapping("/hello") public String hello() throws Exce

Spring Boot从入门到实战:整合通用Mapper简化单表操作

数据库访问是web应用必不可少的部分.现今最常用的数据库ORM框架有Hibernate与Mybatis,Hibernate貌似在传统IT企业用的较多,而Mybatis则在互联网企业应用较多.通用Mapper(https://github.com/abel533/Mapper) 是一个基于Mybatis,将单表的增删改查通过通用方法实现,来减少SQL编写的开源框架,且也有对应开源的mapper-spring-boot-starter提供.我们在此基础上加了一些定制化的内容,以便达到更大程度的复用.

Spring Boot从入门到实战(十):异步处理

原文地址:http://blog.jboost.cn/2019/07/22/springboot-async.html 在业务开发中,有时候会遇到一些非核心的附加功能,比如短信或微信模板消息通知,或者一些耗时比较久,但主流程不需要立即获得其结果反馈的操作,比如保存图片.同步数据到其它合作方等等.如果将这些操作都置于主流程中同步处理,势必会对核心流程的性能造成影响,甚至由于第三方服务的问题导致自身服务不可用.这时候就应该将这些操作异步化,以提高主流程的性能,并与第三方解耦,提高主流程的可用性. 在

Spring中的统一异常处理

在具体的SSM项目开发中,由于Controller层为处于请求处理的最顶层,再往上就是框架代码的.因此,肯定需要在Controller捕获所有异常,并且做适当处理,返回给前端一个友好的错误码. 不过,Controller一多,我们发现每个Controller里都有大量重复的.冗余的异常处理代码,很是啰嗦.能否将这些重复的部分抽取出来,这样保证Controller层更专注于业务逻辑的处理,同时能够使得异常的处理有一个统一的控制中心点. 全局异常处理1.1. HandlerExceptionReso

Spring中的统一异常处理方式

源自:https://segmentfault.com/a/1190000016236188 在具体的SSM项目开发中,由于Controller层为处于请求处理的最顶层,再往上就是框架代码的. 因此,肯定需要在Controller捕获所有异常,并且做适当处理,返回给前端一个友好的错误码. 不过,Controller一多,我们发现每个Controller里都有大量重复的.冗余的异常处理代码,很是啰嗦.能否将这些重复的部分抽取出来,这样保证Controller层更专注于业务逻辑的处理,同时能够使得异

使用Spring MVC统一异常处理实战

1 描写叙述 在J2EE项目的开发中.无论是对底层的数据库操作过程.还是业务层的处理过程,还是控制层的处理过程,都不可避免会遇到各种可预知的.不可预知的异常须要处理.每一个过程都单独处理异常,系统的代码耦合度高,工作量大且不好统一.维护的工作量也非常大. 那么,能不能将全部类型的异常处理从各处理过程解耦出来,这样既保证了相关处理过程的功能较单一,也实现了异常信息的统一处理和维护?答案是肯定的. 以下将介绍使用Spring MVC统一处理异常的解决和实现过程. 2 分析 Spring MVC处理异

spring boot 1.5.4 统一异常处理(九)

上一篇:springboot 1.5.4 配置文件详解(八) 1      Spring Boot统一异常处理 Spring Boot中实现了默认的error映射,但是在实际应用中,上面你的错误页面对用户来说并不够友好,我们通常需要去实现我们自己的异常提示. 以springboot项目为例,进行处理! springboot项目源码: https://git.oschina.net/wyait/springboot1.5.4.git 1.1  创建全局异常处理类 通过使用@ControllerAd