美文网首页Spring MVC
Spring MVC统一异常处理及原理分析

Spring MVC统一异常处理及原理分析

作者: ZX_周雄 | 来源:发表于2019-09-28 12:15 被阅读0次

    文章会从三个方面进行分析:

    1. 提出统一异常处理机制的好处,以及该机制使用姿势
    2. 提供案例:不使用该机制会产生什么样的情况
    3. 机制背后对应的原理分析(重点)

    机制好处及使用姿势

    Spring MVC为我们的WEB应用提供了统一异常处理机制,其好处是:

    1. 业务逻辑和异常处理解耦(业务代码不应该过多地关注异常的处理[职责单一原则])
    2. 消除充斥各处的try catch块代码,使代码更整洁
    3. 便于统一向前端、客户端返回友好的错误提示
    使用姿势如下
    @Slf4j
    @RestControllerAdvice
    public class GlobalExceptionHandler {
    
        @ExceptionHandler
        @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) // Http Status Code 500
        public ResponseDTO handleException(Exception e) {
            // 兜底逻辑,通常用于处理未预期的异常,比如不知道哪儿冒出来的空指针异常
            log.error("", e);
            return ResponseDTO.failedResponse().withErrorMessage("服务器开小差了");
        }
    
        @ExceptionHandler
        @ResponseStatus(HttpStatus.BAD_REQUEST)  // Http Status Code 400
        public ResponseDTO handleBizException(BizException e) {
            // 可预期的业务异常,根据实际情况,决定是否要打印异常堆栈
            log.warn("业务异常:{}", e);
            return ResponseDTO.failedResponse().withErrorMessage(e.getMessage());
        }
    }
    

    注:该demo隐含的前提条件如下

    1. 使用Lombok(当然,也可以手动获取Logger)
    2. GlobalExceptionHandler需要被@ControllerAdvice(Spring 3.2+)或@RestControllerAdvice(Spring 4.3+)注解,并且能够被Spring扫描到

    为配合解释该解决方案,再提供一些基础信息

    1. 业务异常类
    2. 响应信息包装类
    // 1
    public class BizException extends RuntimeException {
        public BizException(String message) {
            super(message);
        }
    }
    
    // 2
    @Data
    public class ResponseDTO<T> implements Serializable {
    
        private static final long serialVersionUID = -3436143993984825439L;
        
        private boolean ok = false;
    
        private T data;
    
        private String errorMessage = "";
    
        public static ResponseDTO successResponse() {
            ResponseDTO message = new ResponseDTO();
            message.setOk(true);
            return message;
        }
    
        public static ResponseDTO failedResponse() {
            ResponseDTO message = new ResponseDTO();
            message.setOk(false);
            return message;
        }
    
        public ResponseDTO withData(T data) {
            this.data = data;
            return this;
        }
    
        public ResponseDTO withErrorMessage(String errorMsg) {
            this.errorMessage = errorMsg;
            return this;
        }
    }
    
    

    案例分析

    案例分析一:
    @GetMapping("/testBizException")
    public ResponseDTO testBizException() {
        if (checkFailed) {
            throw new BizException("test BizException");
        }
    }
    

    当我们请求/testBizException时,该接口在校验失败后抛出了一个BizException,用以代表我们的业务异常,比如参数校验失败(解决方案还有JSR-303的Bean Validation,在此不讨论),优惠券已过期等等业务异常信息。如果没有统一异常处理,我们可能会使用如下方式

    try {
        // check
    } catch (BizException e) {
        return ResponseDTO.failedResponse().withErrorMessage("test BizException");
    }
    

    这种方式,一是不优雅,二是业务逻辑跟异常处理耦合在了一起。

    使用统一异常处理之后,直接抛出业务异常,并提供异常上下文(message + errorCode),代码会流转到GlobalExceptionHandler#handleBizException,统一打印业务日志以及返回错误码和业务异常信息,且Http Status Code 返回400。

    案例分析二:
    @GetMapping("/testUnExpectedException")
    public ResponseDTO testUnExpectedException() {
        int i = 1 / 0;
    }
    

    当我们请求/testUnExpectedException时,该接口会抛出java.lang.ArithmeticException: / by zero,用以代表未预期的异常,比如该案例中的0除异常,仅管此处能一眼辩识出来,但更多的时候,0由变量表示,很容易被忽视,又或者是其它未预期的空指针异常等。当不知道哪里有可能会出异常,又为了前端友好提示,其中一个做法就是try catch大包大揽,将整个方法都try catch住,于是代码产生了腐朽的味道。

    try {
        // do business
    } catch (Exception e) {
        log.error("xxx", e);
        return ResponseDTO.failedResponse().withErrorMessage("服务器开小差");
    }
    

    使用统一异常处理之后,业务代码里不再充斥(滥用)try catch块,只需要关心业务逻辑,当出现不可预期的异常时,代码会流转到GlobalExceptionHandler#handleException,统一打印异常堆栈,以及返回错误码和统一异常信息,且Http Status Code 返回500。

    以上便是Spring MVC为我们提供的统一异常处理机制,我们可以好好加以利用。实际上,该机制在很多公司都在使用,可以从一些开源代码管中窥豹,其中著名的代表就有Apollo,参考com.ctrip.framework.apollo.common.controller.GlobalDefaultExceptionHandler

    原理分析

    了解存在的问题,以及对应的解决方案之后,接下来分析统一异常处理的工作原理

    前提假设:

    1. 原理分析基于Spring Boot 1.5.19.RELEASE,对应的springframework版本为4.3.22.RELEASE
    2. 理解Spring Boot自动装配原理

    先来分析Spring Boot是如何使GlobalExceptionHandler生效的,步骤如下:

    1. 启动类(XXXApplication)@SpringBootApplication注解,而@SpringBootApplication又被@EnableAutoConfiguration所注解,@EnableAutoConfiguration导入EnableAutoConfigurationImportSelector
    2. EnableAutoConfigurationImportSelector实现了ImportSelector接口,其核心方法是selectImports,在该方法中有一行代码是List<String> configurations = getCandidateConfigurations(annotationMetadata,attributes);其含义是通过Spring 的SPI机制,从classpath 所有jar包的META-INF/spring.factories文件中,找到EnableAutoConfiguration对应的"一堆"类[自动装配原理]。这些类作为selectImports的返回值,后期会被Spring加载并实例化,并置入IOC容器中,其中有一项为WebMvcAutoConfiguration
    3. WebMvcAutoConfiguration类存在内部类WebMvcAutoConfigurationAdapter,内部类将导入EnableWebMvcConfiguration(WebMvcConfigurationSupport的子类)
    4. WebMvcConfigurationSupport有个factory methodhandlerExceptionResolver(),该方法向Spring容器中注册了一个HandlerExceptionResolverComposite(实现HandlerExceptionResolver接口),并且默认情况下,给该Composite类添加了三个HandlerExceptionResolver,其中有一个类为ExceptionHandlerExceptionResolver
    5. ExceptionHandlerExceptionResolverInitializingBean的回调方法afterPropertiesSet中,调用initExceptionHandlerAdviceCache()方法进行异常处理器通知缓存的初始化:查找IOC容器中,所有被@ControllerAdvice注解的Bean,如果Bean中存在异常映射,则该Bean会作为key,对应的ExceptionHandlerMethodResolver作为value被缓存起来
    6. ExceptionHandlerMethodResolver是真正干活的类,用于解析被@ExceptionHandler注解的方法,保存异常类及对应的异处常理方法<exceptionType, method>。对应到上述案例一,保存的是BizExceptionhandleBizException()方法的映射关系,表明:当业务代码抛出BizException时,会由handleBizException()进行处理
    private void initExceptionHandlerAdviceCache() {
        ...
        
        List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
        AnnotationAwareOrderComparator.sort(adviceBeans);
    
        for (ControllerAdviceBean adviceBean : adviceBeans) {
            ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(adviceBean.getBeanType());
            if (resolver.hasExceptionMappings()) {
                this.exceptionHandlerAdviceCache.put(adviceBean, resolver);
                ...
            }
            
            ...
        }
    }
    

    应用启动完毕之后,GlobalExceptionHandler已经生效,即exceptionHandlerAdviceCache已经缓存了异常处理器及其对应的ExceptionHandlerMethodResolver,一旦发生了异常,会从exceptionHandlerAdviceCache里依次判断哪个异常处理器可以用,并找到对应的异常处理方法进行异常的处理。

    接着分析异常处理的具体流程,当一个Controller方法中抛出异常后,步骤如下:

    1. org.springframework.web.servlet.DispatcherServlet#doDispatch会catch住异常,并调用processDispatchResult();方法进行异常的处理
    // DispatcherServlet#processHandlerException
                
    if (exception != null) {
        if (exception instanceof ModelAndViewDefiningException) {
            logger.debug("ModelAndViewDefiningException encountered", exception);
            mv = ((ModelAndViewDefiningException) exception).getModelAndView();
        }
        else {
            Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
            mv = processHandlerException(request, response, handler, exception);
            errorView = (mv != null);
        }
    }
    
    // DispatcherServlet#processHandlerException
    
    for (HandlerExceptionResolver handlerExceptionResolver : this.handlerExceptionResolvers) {
        exMv = handlerExceptionResolver.resolveException(request, response, handler, ex);
        if (exMv != null) {
            break;
        }
    }
    

    这里的this.handlerExceptionResolvers是在Spring Boot启动的过程中初始化的,其中就包含上述启动步骤4中的HandlerExceptionResolverComposite。因此,这里会调用HandlerExceptionResolverCompositeresolveException方法进行异常的处理

    1. XXXComposite在Spring中是个组合类,一般内部会维护一个由Composite父接口实例构成的列表,如HandlerExceptionResolverComposite实现了HandlerExceptionResolver接口,其内部维护了一个HandlerExceptionResolver集合。HandlerExceptionResolverCompositeresolveException方法同样是迭代其内部维护的集合,并依次调用其resolveException方法进行解析,
      其内部集合中有一个ExceptionHandlerExceptionResolver实例,且首先会进入该实例进行处理
    // HandlerExceptionResolverComposite#resolveException
    
    public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler,Exception ex) {
        if (this.resolvers != null) {
            for (HandlerExceptionResolver handlerExceptionResolver : this.resolvers) {
                ModelAndView mav = handlerExceptionResolver.resolveException(request, response, handler, ex);
                if (mav != null) {
                    return mav;
                }
            }
        }
        return null;
    }
    
    1. 根据抛出的异常类型,拿到异常处理器及对应的异常处理方法,并转化成ServletInvocableHandlerMethod,并执行invokeAndHandle方法,也即是说,最终会转换成执行异常处理器的异常处理方法。(org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod#invokeAndHandle 是Spring MVC处理Http请求中的重要方法,篇幅原因不在此介绍其原理)
    // ExceptionHandlerExceptionResolver#doResolveHandlerMethodException
    
    ServletInvocableHandlerMethod exceptionHandlerMethod = getExceptionHandlerMethod(handlerMethod, exception);
    
    ...
    
    exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, exception, handlerMethod);
    
    ...
    
    // ExceptionHandlerExceptionResolver#getExceptionHandlerMethod
    
    // 这段逻辑是@ExceptionHandler写在Controller类里的处理方式,这种方式不通用也不常用,不做介绍
    ...
    
    for (Map.Entry<ControllerAdviceBean, ExceptionHandlerMethodResolver> entry : this.exceptionHandlerAdviceCache.entrySet()) {
        ControllerAdviceBean advice = entry.getKey();
        // @ControllerAdvice注解可以指定仅拦截某些类,这里判断handlerType是否在其作用域内
        if (advice.isApplicableToBeanType(handlerType)) {
            ExceptionHandlerMethodResolver resolver = entry.getValue();
            Method method = resolver.resolveMethod(exception);
            if (method != null) {
                return new ServletInvocableHandlerMethod(advice.resolveBean(), method);
            }
        }
    }
    

    根据异常,找到异常处理方法

    // ExceptionHandlerMethodResolver#resolveMethodByExceptionType
    
    public Method resolveMethod(Exception exception) {
        Method method = resolveMethodByExceptionType(exception.getClass());
        if (method == null) {
            Throwable cause = exception.getCause();
            if (cause != null) {
                method = resolveMethodByExceptionType(cause.getClass());
            }
        }
        return method;
    }
    
    // ExceptionHandlerMethodResolver#resolveMethodByExceptionType
    
    public Method resolveMethodByExceptionType(Class<? extends Throwable> exceptionType) {
        Method method = this.exceptionLookupCache.get(exceptionType);
        if (method == null) {
            // 核心方法
            method = getMappedMethod(exceptionType);
            this.exceptionLookupCache.put(exceptionType, (method != null ? method : NO_METHOD_FOUND));
        }
        return (method != NO_METHOD_FOUND ? method : null);
    }
    
    private Method getMappedMethod(Class<? extends Throwable> exceptionType) {
        List<Class<? extends Throwable>> matches = new ArrayList<Class<? extends Throwable>>();
        for (Class<? extends Throwable> mappedException : this.mappedMethods.keySet()) {
            if (mappedException.isAssignableFrom(exceptionType)) {
                matches.add(mappedException);
            }
        }
        // 如果找到多个匹配的异常,就排序之后取第一个(最优的)
        if (!matches.isEmpty()) {
            Collections.sort(matches, new ExceptionDepthComparator(exceptionType));
            return this.mappedMethods.get(matches.get(0));
        }
        else {
            return null;
        }
    }
    

    案例中,我们的mappedException有两个:BizExceptionException,都满足mappedException.isAssignableFrom(exceptionType)条件,均会被加入matches中,经过排序之后,"最匹配"的BizException会排在matchs集合的第一个位置,所以会选择它所对应的异常处理方法返回。因此,"最匹配"的关键点就在于比较器ExceptionDepthComparator,根据类名,可以推测其出比较的依据是目标异常类与待排序异常类的"深度"。

    举个例子,假设目标异常类为BizException,而待排序的集合中分别有BizExceptionRuntimeExceptionException,那么他们之间的深度分别为0,1,2,因此,排序之后,集合中的BizException与目标异常类BizException最为匹配,排在了集合中首位,RuntimeException次匹配,排在了集合的第二位,Exception最不匹配,排在集合的第三位。

    public int compare(Class<? extends Throwable> o1, Class<? extends Throwable> o2) {
        int depth1 = getDepth(o1, this.targetException, 0);
        int depth2 = getDepth(o2, this.targetException, 0);
        return (depth1 - depth2);
    }
    
    private int getDepth(Class<?> declaredException, Class<?> exceptionToMatch, int depth) {
        if (exceptionToMatch.equals(declaredException)) {
            // Found it!
            return depth;
        }
        // If we've gone as far as we can go and haven't found it...
        if (exceptionToMatch == Throwable.class) {
            return Integer.MAX_VALUE;
        }
        return getDepth(declaredException, exceptionToMatch.getSuperclass(), depth + 1);
    }
    

    总结:

    1. Spring Boot应用启动时,会扫描被@ControllerAdvice注解的Bean,找到其内部被@ExceptionHandler注解的方法,解析其所能处理的异常类,并缓存到exceptionHandlerAdviceCache

    2. 当HTTP请求在Controller中发生异常,会被DispatcherServlet捕获,并调用ExceptionHandlerExceptionResolver#resolveException进行异常的解析,解析的过程依赖exceptionHandlerAdviceCache进行真正的异常处理方法的查找,找到之后封装成ServletInvocableHandlerMethod,然后被Spring进行调用,也即是会回调到我们的异常处理器的异常处理方法之中,即处理了异常。

    注:

    本文限于篇幅原因,不会面面俱到,只重点分析统一异常处理器的生效过程,以及作用过程,摘出其中重点的代码进行分析而忽略了其中的一些分支情况,读者们可自行跟踪代码看看其中的细节处理。

    相关文章

      网友评论

        本文标题:Spring MVC统一异常处理及原理分析

        本文链接:https://www.haomeiwen.com/subject/zuosuctx.html