美文网首页征服Springssh+Spring(Boot/MVC)
Spring @ExceptionHandler 运行原理分析

Spring @ExceptionHandler 运行原理分析

作者: 东阿 | 来源:发表于2019-05-22 16:03 被阅读0次

    关键词: JAVA, Spring, SpringMVC

    spring exception handler因使用简单,代码简介,优雅,广受开发人员喜爱。
    使用出来的效果如下:


    image.png

    (图是盗的,如何使用 不是本文讨论的重点。)

    此处就来 揭示下其工作原理。

    ExceptionHandler 的运行效果有 以下特点。

    1. 一次声明,全接口生效。
    2. Exception的匹配 符合" 就近原则 "

    现在我们就来依次探寻。

    首先是 第一条。

    众所周知的是,springMVC 基于servelet ,Spring mvc 的 总入口 就是。 org.springframework.web.servlet.DispatcherServlet。

    异常捕获的伪代码为

                            dispatchException=null;
                try {
                    response = handler(request);
                }
                catch (Exception ex) {
                    dispatchException = ex;
                }
                catch (Throwable err) {
                    dispatchException = new NestedServletException("Handler dispatch failed", err);
                }
                processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
    

    spring 所有 映射接口 的 入口 从 DispatcherServlet 进入,在执行的时候 会 捕获所有异常
    需要注意的是,仅捕获 Exception。 所有的Error 将被 NestedServletException 进行包装
    包装为 NestedServletException。所以平时开发的时候 应当注意处理 spring的 NestedServletException。

    比较神奇的 是第二条。
    Exception的匹配 符合" 就近原则 "。

    平时我们开发的时候,一个try,写连续的多个 catch。catch处理块的 编写顺序应当是 先写子类后写父类。
    用来让错误被正确的解析。
    平时 catch 是否匹配的 逻辑 在于,根据先后顺序,依次向下匹配,当抛出的异常 为catch 声明的 异常的 子类或类型相同,则认为匹配 。

    spring 是怎么处理的?

    遍地的 exception,以及 各种 exceptionHandler,是怎么做到 一处声明 到处处理。还能遵从子类优先原则。

    简单的想法是,将所有的 exception 串起来,写一个长长的 catch。如

    try{
     //do
    }catch(E1){
    
    }
    catch(E1){
    
    }
    catch(E2){
    
    }
    catch(E3){
    
    }
    catch(E4){
    
    }
    catch(E5){
    
    }
    

    那么问题来了,
    1 这个顺序 怎么定。
    2 我们必然需要将要处理的所有异常类型放在 一个 集合中,遍历后 catch。显然 java 并没有优雅的提供这种循环catch的操作。而且比较傻。

    我们的异常通常会进行分类 EA EB。 过一段时间 又对 EA 扩展为 EA1 EA2 EA3 。对EB 也扩展出了EB2.
    EA 为 EA1 EA2 EA3 的父类。
    当异常发生的 时候(如EA1),按照上述逻辑,一个异常 要 去 EA EA1 EA2 EA3 EB EB2等所有 catch块都进行一次匹配,寻找正确的处理器。
    实际上 EA1 的处理器,只可能 被 EA1 与 EA 的异常处理器 所解析。其他的遍历 显然是 不必要的。
    同时 处理的 时候,应当必须 EA1的处理器被先匹配。

    那应当怎样操作?

    最好的方式 当然是,直接一个 hashMap key为异常类型,value 是 异常的处理程序。
    EA1发生时,直接根据map找到 EA1的异常处理程序。
    现实生活显然不是这样的 因为 EA1 可能没有自己的专属 异常处理程序。那么就要找到一个最近的 EA的异常处理程序。

    这个算法具体出来就是这个核心算法;

        private final Map<Class<? extends Throwable>, Method> mappedMethods =
                new ConcurrentHashMap<Class<? extends Throwable>, Method>(16);
    
        private final Map<Class<? extends Throwable>, Method> exceptionLookupCache =
                new ConcurrentHashMap<Class<? extends Throwable>, Method>(16);
        /**
         * Find a {@link Method} to handle the given exception type. This can be
         * useful if an {@link Exception} instance is not available (e.g. for tools).
         * @param exceptionType the exception type
         * @return a Method to handle the exception, or {@code null} if none found
         */
        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);
        }
    
        /**
         * Return the {@link Method} mapped to the given exception type, or {@code null} if none.
         */
        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;
            }
        }
    
    

    首先程序会在一开始 加载的时候将所有的异常处理方法扫描添加至 mappedMethods 中。所有的。
    当一个新的 需要处理的异常 来的 时候 。
    首先从刚才的 mappedMethods 这个 map 中 全部遍历一遍 ,要求是 isAssignableFrom。就是把能处理此异常的 异常处理程序筛选出来。
    将筛选后的结果 放入list中,然后根据特定的排序器排序,取第一个 异常处理器。
    得到后,将此次结果 放入exceptionLookupCache。下一次 当有一样的异常 来了的 时候,就直接从这个 map 中找到对应的异常处理程序。

    那么排序又是怎么实现的呢?想来,应该是子类优先的机制。因为前面 的 list 都是 有父子关系的 异常,所有理论上只要根据子类优先的排序方法,子类在前,父类在后,然后取第一个,自然是 “最近原则” 所匹配的异常处理程序。

    实际代码要相对复杂一些,如下

        @Override
        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);
        }
    

    相关文章

      网友评论

        本文标题:Spring @ExceptionHandler 运行原理分析

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