• 一文带你掌握Spring Web异常处理方式


    一、前言

    大家好,我是 去哪里吃鱼 ,也叫小张。

    最近从单位离职了,离开了五年多来朝朝夕夕皆灯火辉煌的某网,激情也好悲凉也罢,觥筹场上屡屡物是人非,调转过事业部以为能换种情绪,岂料和下了周五的班的前同事兼好朋友,匆匆赶往藏身巷弄的小菜馆里时,又次次被迫想起,那破晓时分大厦头顶有点吝啬的阳光。

    阿坤:但凡拿我们当自己人,就不会这样...
    我 :也许人家想好好表现呢
    阿坤:算了,不说了,走着走着天要亮了,回去睡吧
    我 :卧槽,真的是,行了不说了,趁着下面还没亮,赶紧回去睡吧
    阿坤:下午见

    小张目前蜗居赋闲,顺便养一下左肩。

    想念七七。

    扯远了,不说了,今天来给大家说一下 Spring Web 模块(基于 Servlet)中的异常(以下简称 Spring 异常)处理机制,看完文章,你应该会懂得在 web 开发过程中,怎么处理程序出现的异常。

    本文基于 springboot 2.5.1 , 对应 spring framework 版本为 5.3.8

    二、本文的异常种类划分

    1. "你妹啊,谁在 service 里面抛了个自定义异常给我 controller !" ———— 业务代码引起的异常
    2. "你这报文签名不对,参数也不对,拦截器都没过" ———— 拦截器异常
    3. "这是什么错误啊,status=500,啥也没有显示啊" ———— errorPath 异常

    三、Spring Web 模块的请求核心流程解析

    上述错误,都是用户在使用浏览器或者 APP 等访问后台时候出现的异常,因此我们有必要去了解一下 Spring Web 模块对用户请求的核心处理流程,只有当熟悉了请求处理流程,我们处理起异常来,才会得心应手。

    3.1 那个 Servlet

    每一个基于 Servlet 的 web 框架,都会有自己的 Servlet 实现,在 Spring Web 中,它叫 DispatcherServlet ,你所有的请求都会经过它来处理。

    而在 Spring 的设计中,DispatcherServlet 中处理请求的那个方法,叫 doDispatch()

    3.2 那个 doDispatch()

    话不多说,先来看我精简过的方法

    protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
            HttpServletRequest processedRequest = request;
            HandlerExecutionChain mappedHandler = null;
            // 。。。小张替你省略部分代码。。。
            try {
                ModelAndView mv = null;
                Exception dispatchException = null;
    
                try {
                    // 。。。小张替你省略部分代码。。。
    
                    // 根据 request 获取对应的 Handler
                    mappedHandler = getHandler(processedRequest);
                    if (mappedHandler == null) {
                        noHandlerFound(processedRequest, response);
                        return;
                    }
    
                    // 根据 Handler 类型找到合适的 Handler 适配器,DispatcherServilet 通过适配器间接调用 Handler ,
                    HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
    
                    // 。。。小张替你省略部分代码。。。
    
                    // 重头戏来了
                    // 步骤1. 下面这一行会遍历拦截器,执行所有拦截器的 preHandle() 方法
                    if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                        return;
                    }
    
                    // 步骤2. 当所有拦截器都校验通过的时,下面这一行执行目标 controller 对应的业务方法
                    mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
    
                    // 。。。小张替你省略部分代码。。。
    
                    // 步骤3. 当目标 controller 的业务方法执行完毕之后,下面这一行执行所有拦截器的 postHandler() 方法
                    mappedHandler.applyPostHandle(processedRequest, response, mv);
    
                    // 步骤4. 下面有两个异常,捕获了 拦截器 preHandler 阶段和 controller 的业务方法执行阶段抛出的异常
                } catch (Exception ex) {
                    dispatchException = ex;
                } catch (Throwable err) {
                    dispatchException = new NestedServletException("Handler dispatch failed", err);
                }
                // 步骤5. 如果步骤4有异常,就会在这里处理
                processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
    
                // 步骤6. 下面两个异常调用所有拦截器的 fterCompletion方法
            } catch (Exception ex) {
                triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
            } catch (Throwable err) {
                triggerAfterCompletion(processedRequest, response, mappedHandler,new NestedServletException("Handler processing failed", err));
            }finally {
                // 。。。小张替你省略部分代码。。。
            }
        }
    

    上面代码中,最里面的那个 try ... catch 已经把常用情况下的 拦截器、controller 的异常捕获到了,异常处理逻辑在 步骤5 里面:

    private void processDispatchResult(HttpServletRequest request, 
                                        HttpServletResponse response, 
                                        @Nullable HandlerExecutionChain mappedHandler, 
                                        @Nullable ModelAndView mv, 
                                        @Nullable Exception exception) throws Exception { 
        // 。。。小张替你省略部分代码。。。
    
        if (exception != null) {
            if (exception instanceof ModelAndViewDefiningException) {
               // 。。。小张替你省略部分代码。。。
            } else {
                Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
                // 调用 DispatcherServlet 异常处理流程
                mv = processHandlerException(request, response, handler, exception);
                errorView = (mv != null);
            }
        }
        // 。。。小张替你省略部分代码。。。
    }
    
    ModelAndView processHandlerException(HttpServletRequest request,
                                        HttpServletResponse response,
                                        @Nullable Object handler,
                                        Exception ex) throws Exception {
        ModelAndView exMv = null;
        if (this.handlerExceptionResolvers != null) {
            // 遍历 DispatcherServlet 里加载好的异常处理器
            for (HandlerExceptionResolver handlerExceptionResolver : this.handlerExceptionResolvers) {
                // 交给异常处理器处理
                exMv = handlerExceptionResolver.resolveException(request, response, handler, ex);
                if (exMv != null) {
                    // 找到合适的异常处理器就中断
                    break;
                }
            }
        }
        // 。。。小张替你省略部分代码。。。
    }
    

    异常最终会交给 DispatcherServlet 里的 this.handlerExceptionResolvers 集合来处理,而这个东西也是我们自己规划的异常处理器最终汇聚的地方,它的类型是 HandlerExceptionResolver 接口

    3.3 那个 HandlerExceptionResolver

    这是一个接口,只有异常处理的方法签名

    public interface HandlerExceptionResolver {
    
        @Nullable
        ModelAndView resolveException(
                HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex);
    
    }
    

    注意,返回值 ModelAndView 不为空,证明该异常处理器处理了异常,spring 不会再让剩下的异常处理器处理该异常

    四、异常处理手段

    章节二的划分对应的处理手段有下面这几种,我们一一举例

    4.1 controller 中的业务代码引起的异常处理方式

    4.1.1 简单点,使用 @ControllerAdvice 注解和 @ExceptionHandler 注解

    // 步骤1. 使用注解修饰异常处理类
    @ControllerAdvice
    public class ErrorHandlerDemo {
    
        // 步骤2. 搭配使用注解,处理指定异常
        @ExceptionHandler(CacheException.class)
        @ResponseBody
        public String cacheException(HandlerMethod handlerMethod, Exception e) {
            return defaultErrorHandler(handlerMethod, e);
        }
    
        // 步骤2. 搭配使用注解,处理指定异常
        @ExceptionHandler(value = Exception.class)
        @ResponseBody
        public String defaultErrorHandler(HandlerMethod handlerMethod, Exception e) {
            return revertMessage(e);
        }
    
        private String revertMessage(Exception e) {
            String msg = "系统异常";
            if (e instanceof CacheException) {
                msg = e.getMessage();
            }
            return msg;
        }
    }
    

    对应异常处理的方法返回值类型,类比 @RequestMapping 方法的返回值类型,比如,也可以是 ModelAndView 类型

    原理剖析

    A. @ControllerAdvice + @ExceptionHandler 注解修饰的类的解析

    首先,被 @ControllerAdvice 注解修饰的类,会被 Spring 包装成 ControllerAdviceBean ,这个东西把修饰的类的 Class 保存成 beanType ,并且是 ExceptionHandlerMethodResolver 的构造函数入参,唯一的构造函数唯一的入参。

    ExceptionHandlerMethodResolver 又是个什么东西? 异常处理器方法解析器?什么玩意儿!先看它都干了什么吧

    public class ExceptionHandlerMethodResolver {
    
    public static final MethodFilter EXCEPTION_HANDLER_METHODS = method -> AnnotatedElementUtils.hasAnnotation(method, ExceptionHandler.class);
    
        // 参数 handlerType 就是上面说提到的 beanType,就是上面实例代码中的 ErrorHandlerDemo 类
        public ExceptionHandlerMethodResolver(Class handlerType) {
    
            // 这个 EXCEPTION_HANDLER_METHODS 是个函数式接口
            // MethodIntrospector.selectMethods() 方法用来查找 @ControllerAdvice 类里面被 @ExceptionHandler 注解修饰的方法并缓存起来
            for (Method method : MethodIntrospector.selectMethods(handlerType, EXCEPTION_HANDLER_METHODS)) {
                for (Classextends Throwable> exceptionType : detectExceptionMappings(method)) {
    
                    // 以 异常类型:异常处理方法 格式缓存起来
                    addExceptionMapping(exceptionType, method);
                }
            }
        }
    
    }
    

    细心读下注释,可以发现 ExceptionHandlerMethodResolver 已经把我们自定义的异常处理类和异常处理方法都已经收集、准备完毕了。

    有人说了,我搞了多个 @ControllerAdvice 修饰的类啊,你敢不敢都给解析了?

    敢!接下来就告诉你 @ControllerAdvice 修饰的类在哪里解析的!

    B. @ControllerAdvice + @ExceptionHandler 注解修饰的类的加载

    有个类叫 ExceptionHandlerExceptionResolver (什么玩意儿?异常处理器异常解析器?),别懵,不翻译它,看它是个啥

    public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExceptionResolver
            implements ApplicationContextAware, InitializingBean {
        @Nullable
        private ApplicationContext applicationContext;
    
        // 没错,就是这里,缓存了 ErrorHandlerDemo 和它内部的异常处理方法对应的 ExceptionHandlerMethodResolver
        private final Map exceptionHandlerAdviceCache = new LinkedHashMap<>();
    
        // 。。。小张替你省略部分代码。。。
    
        // 这里是 InitializingBean 的实现方法
        @Override
        public void afterPropertiesSet() {
            // 这就是加载 ControllerAdviceBean 的地方
            initExceptionHandlerAdviceCache();
    
            // 。。。小张替你省略部分代码。。。
        }
    
        private void initExceptionHandlerAdviceCache() {
            if (getApplicationContext() == null) {
                return;
            }
    
            // 加载重点来了,通过上 Spring 上下文获取被 @ControllerAdvice 修饰的 ErrorHandlerDemo
            List adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
    
            for (ControllerAdviceBean adviceBean : adviceBeans) {
                Class beanType = adviceBean.getBeanType();
                if (beanType == null) {
                    throw new IllegalStateException("Unresolvable type for ControllerAdviceBean: " + adviceBean);
                }
    
                // 创建与 ErrorHandlerDemo 中的异常处理方法对应的 ExceptionHandlerMethodResolver,就是上面A小节的解析部分
                ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(beanType);
    
                if (resolver.hasExceptionMappings()) {
                    // ControllerAdviceBean 与 ExceptionHandlerMethodResolver 一一对应,缓存起来
                    this.exceptionHandlerAdviceCache.put(adviceBean, resolver);
                }
    
                // 。。。小张替你省略部分代码。。。
            }
    
            // 。。。小张替你省略部分代码。。。
        }
    }
    

    那么到这里就明白了, ExceptionHandlerExceptionResolver 里面缓存了 ControllerAdivceBean 和它对应的具体的异常处理方法包装(即 ExceptionHandlerMethodResolver)。

    读到这里,也许朋友你会问, ExceptionHandlerExceptionResolver 是缓存了,但是,这个玩意儿怎么用的呢,在哪里用的呢?

    C. @ControllerAdvice + @ExceptionHandler 的启用

    在 spring-webmvc 模块中,有个类叫 WebMvcConfigurationSupport 它用来支持 web 的相关配置,他有一个创建 Bean 的方法

    public class WebMvcConfigurationSupport implements ApplicationContextAware, ServletContextAware {
        // 。。。小张替你省略部分代码。。。
    
        /**
        * 创建异常处理器组合
        **/
        @Bean
        public HandlerExceptionResolver handlerExceptionResolver(@Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager) {
            List exceptionResolvers = new ArrayList<>();
            // 这里是空方法1,可以自定义实现,一般不拓展这个方法
            configureHandlerExceptionResolvers(exceptionResolvers);
            // 如果没有重写上面的方法,则会走这里,创建默认的异常处理器
            if (exceptionResolvers.isEmpty()) {
                addDefaultHandlerExceptionResolvers(exceptionResolvers, contentNegotiationManager);
            }
            // 这里是空方法2,可以自定义实现,一般拓展这个方法往所给的异常处理器集合里添加自定义异常处理器
            extendHandlerExceptionResolvers(exceptionResolvers);
    
            // 把异常处理器集合组装到 HandlerExceptionResolverComposite 里, 而 HandlerExceptionResolverComposite  是接口 HandlerExceptionResolver 的实现类
            HandlerExceptionResolverComposite composite = new HandlerExceptionResolverComposite();
            composite.setOrder(0);
            composite.setExceptionResolvers(exceptionResolvers);
            return composite;
        }
    
        /**
        * 根据提供的异常处理器创建异常处理器组合
        **/
        protected final void addDefaultHandlerExceptionResolvers(List exceptionResolvers,
                ContentNegotiationManager mvcContentNegotiationManager) {
            //创建 B 章节里的异常处理器
            ExceptionHandlerExceptionResolver exceptionHandlerResolver = createExceptionHandlerExceptionResolver();
            // 。。。小张替你省略部分代码。。。
    
            // 调用 InitializingBean 接口方法
            exceptionHandlerResolver.afterPropertiesSet();
            // 添加创建的异常处理器到集合中
            exceptionResolvers.add(exceptionHandlerResolver);
    
            // 。。。小张替你省略部分代码。。。
        }
    
        // 直接 new 了一个 ExceptionHandlerExceptionResolver
        protected ExceptionHandlerExceptionResolver createExceptionHandlerExceptionResolver() {
            return new ExceptionHandlerExceptionResolver();
        }
    }
    

    上面的 @Bean 创建方法做了下面这些事

    1. 提供异常处理器自定义拓展方法 configureHandlerExceptionResolvers()extendHandlerExceptionResolvers()

    2. 如果没有指定异常处理器,只是拓展异常处理器,则创建默认异常处理器 ExceptionHandlerExceptionResolver

    3. 根据提供的异常处理器创建处理器组合对象 HandlerExceptionResolverComposite ,其也是异常处理器

    到此为止 @Bean 方法已经做完了异常处理器的整合过程,常常与 @Bean 方法搭配使用的,是 @Configuration 注解修饰的配置类,然而 WebMvcConfigurationSupport 并没有这个注解

    鸡贼的 spring 把 @Configuration 注解放到了它的子类 DelegatingWebMvcConfiguration 上!

    package org.springframework.web.servlet.config.annotation;
    
    // 配置类,自动加载
    @Configuration(proxyBeanMethods = false)
    public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
    
        private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite();
    
        // 这个 WebMvcConfigurer 就是我们在 web 项目中自定义拦截器、异常处理器等需要实现的接口
        @Autowired(required = false)
        public void setConfigurers(List configurers) {
            if (!CollectionUtils.isEmpty(configurers)) {
                this.configurers.addWebMvcConfigurers(configurers);
            }
        }
    }
    

    到这里,使用 @ControllerAdvice 注解和 @ExceptionHandler 注解来处理 Controller 异常的整个加载流程已经剖析完毕了

    4.1.2 自定义 HandlerExceptionResolver

    [章节 3.3 ](## 3.3 HandlerExceptionResolver)(markdown 什么时候原生支持页面内跳转)已经有了接口简单描述,我们直接来个接口实现类 demo

    public class ExceptionHandlerDemo implements HandlerExceptionResolver {
    
        private final ModelAndView EMPTY_MODEL_VIEW = new ModelAndView();
    
        @Override
        public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception e) {
            // 自定义异常处理逻辑,可以输出状态到 response
            // 记得返回一个空 ModelAndView,证明异常已经被处理
            return EMPTY_MODEL_VIEW;
        }
    }
    

    现在实现类有了, 我们把它加载到 spring 的 web 环境中去

    // 配置类
    @Configuration
    public class WebConfigDemo implements WebMvcConfigurer {
    
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(new IpInterceptor()).addPathPatterns(
                    Arrays.asList(
                            "/index",
                            "/apply",
                            "/product/i",
                            "/product/d/*"
                    ));
        }
    
        // 就是这里,添加异常处理器
        @Override
        public void extendHandlerExceptionResolvers(List resolvers) {
            resolvers.add(new ExceptionHandlerDemo());
        }
    }
    

    嗯,就是这样简单。

    什么?你问我加载进去之后怎么生效的?

    在 章节 4.1.1 中的 C 小节有提到类 DelegatingWebMvcConfiguration ,它的 setConfigurers(List configurers) 方法自动注入了咱们的 WebConfigDemo 配置类,并且,它重写了其父类 WebMvcConfigurationSupport 中的 extendHandlerExceptionResolvers()configureHandlerExceptionResolvers() 方法

    @Configuration(proxyBeanMethods = false)
    public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
    
        // 当作是一个 配置类集合
        private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite();
    
        // 注入 WebConfigDemo
        @Autowired(required = false)
        public void setConfigurers(List configurers) {
            if (!CollectionUtils.isEmpty(configurers)) {
                this.configurers.addWebMvcConfigurers(configurers);
            }
        }
    
        @Override
        protected void configureHandlerExceptionResolvers(List exceptionResolvers) {
            this.configurers.configureHandlerExceptionResolvers(exceptionResolvers);
        }
    
        // 加载 ExceptionHandlerDemo
        @Override
        protected void extendHandlerExceptionResolvers(List exceptionResolvers) {
            this.configurers.extendHandlerExceptionResolvers(exceptionResolvers);
        }
    
        // 。。。小张替你省略部分代码。。。
    }
    

    这样在章节 4.1.1 中 C 小节, 执行 @Bean 方法 handlerExceptionResolver() 方法时候,空方法2 就指向了这里,进而加载到自定义的 ExceptionHandlerDemo

    4.2 拦截器中抛出的异常

    拦截器有3个方法签名

    public interface HandlerInterceptor {
    
        default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
                throws Exception {
            return true;
        }
    
    
        default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
                @Nullable ModelAndView modelAndView) throws Exception {
        }
    
    
        default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
                @Nullable Exception ex) throws Exception {
        }
    
    }
    

    其中针对方法 afterCompletion() 抛出的异常,spring 只是简单打印了一个错误日志,并没有处理,也许 spring 认为,到这里,请求内容已经处理完了,所以不再把错误返回给调用方

    void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, @Nullable Exception ex) {
        for (int i = this.interceptorIndex; i >= 0; i--) {
            HandlerInterceptor interceptor = this.interceptorList.get(i);
            try {
                interceptor.afterCompletion(request, response, this.handler, ex);
            } catch (Throwable ex2) {
                // 仅打印错误日志
                logger.error("HandlerInterceptor.afterCompletion threw exception", ex2);
            }
        }
    }
    

    那么剩下的两个方法,其实章节 3.2 当中已经指明了,preHandle()postHandle 方法内出现的异常,与 controller 实体请求中的异常一起被处理了,所以章节 4.1 当中的异常处理方式,对于拦截器异常,同样生效。

    4.3 errorPath 异常

    介绍 errorPath 之前,先说一下 spring 对于未捕获到的异常的处理方式

    对于未捕获到的异常,spring 会返回 500 http 状态码给调用方,并且转发请求到一个指定地址,这个地址默认值为 /error

    在 spring boot 中的默认配置为 server.error.path=/error

    以上,小张姑且称之为:错误转发机制 ,其实不仅仅是 500 状态,404 状态也会转发,你还能再找出些状态吗 ?

    那么我们可以自已实现一个 /error controller 来处理异常吗?可以的,得益于 SpringBoot,我们可以借助另外一个叫 ErrorAttributes 的 bean 来获取异常信息

    当请求出现异常时候,我们可以从 Request 当中读取出来

    @Controller
    public class ErrorHandleController implements ErrorController {
    
        private final ErrorAttributes errorAttributes;
    
        // 注入 SpringBoot 已经替我们创建好的 ErrorAttributes
        public ErrorHandleController(ErrorAttributes errorAttributes) {
            this.errorAttributes = errorAttributes;
        }
    
        @RequestMapping(value = "/error", produces = "text/html")
        @ResponseBody
        public String errorPage(HttpServletRequest request) {
            return this.getErrorAttributesMapString(request);
        }
    
    
        @RequestMapping(value = "/error")
        @ResponseBody
        public String errorHandler(HttpServletRequest request) {
            return this.getErrorAttributesMapString(request);
        }
    
        private String getErrorAttributesMapString(HttpServletRequest request) {
            ServletWebRequest webRequest = new ServletWebRequest(request);
            // 利用 ErrorAttributes 读取 request 当中的异常,这里仅仅是简单地打印到页面上
            return this.errorAttributes.getErrorAttributes(webRequest, ErrorAttributeOptions.defaults()).toString();
        }
    
    }
    

    五、结尾

    本文并没有提供相应的 demo 演示,只是侧重于带领大家把 spring 的异常处理从头到尾过一遍,如果想实验,自己动手,结果会更快乐的。

    有疑问的同学,欢迎评论区留言交流。

    想念七七。

  • 相关阅读:
    已解决(pandas读取DataFrame列报错)raise KeyError(key) from err KeyError: (‘name‘, ‘age‘)
    Sql力扣算法:185. 部门工资前三高的所有员工
    什么是面向对象编程
    构建动态交互式H5导航栏:滑动高亮、吸顶和锚点导航技巧详解
    easy-excel 解决百万数据导入导出,性能很强
    使用Docker部署debezium来监控 MySQL 数据库
    深入浅出带你了解PHAR反序列化
    聊聊公司的那点事
    【JavaScript】写程序编程基础入门
    redis
  • 原文地址:https://www.cnblogs.com/qnlcy/p/16573098.html