相关推荐recommended
Spring-异常处理(6种讲解)
作者:mmseoamin日期:2024-02-24

目录

控制器级@ExceptionHandler

HandlerExceptionResolver接口

使用注解实现异常分类管理(@ControllerAdvice 和 @ExceptionHandler)

使用 @ControllerAdvice 对不同的 Controller 分别捕获异常并处理

ResponseStatusException(Spring 5及以上版本)

Spring Web MVC 中的异常处理流程


控制器级@ExceptionHandler

  • 第一个解决方案是在@Controller级别工作
  • 我们将定义一个方法来处理异常并使用@ExceptionHandler进行注解:

    Spring-异常处理(6种讲解),第1张

    • 这种方法有一个主要缺点:@ExceptionHandler注解的方法仅对特定的 Controller 有效,而不是对整个应用程序全局有效,将其添加到每个控制器使其进行处理异常,不太适合通用异常处理机制
    • 我们可以通过让所有控制器扩展基本控制器类来解决此限制
    • 然而此解决方案可能是一个问题
    • 例如,控制器可能已经从另一个基类扩展,该基类可能位于另一个 jar 中或不可直接修改,或者本身可能不可直接修改
    • 接下来,我们将研究另一种解决异常处理问题的方法 - 一种全局的方法

      HandlerExceptionResolver接口

      • 在SpringMVC中,提供了一个全局异常处理器,用于对系统中出现的异常进行统一处理
      • 在一般的系统中,DAO层、Service层及Controller层出现异常都以“throws Exception”的形式向上层抛出,最后都会由SpringMVC的前端控制器(DispatcherServlet)统一交给全局异常处理器进行异常处理
      • 在SpringMVC中提供的HandlerExceptionResolver接口可以实现全局异常处理器
      • Spring MVC接口HandlerExceptionResolver用于抽象一个异常解析器
      • 这种异常解析器被用于分析请求处理器Handler映射或者执行过程中的异常,将这些异常转换成其他形式展示给用户

        Spring-异常处理(6种讲解),第2张

        • 可以看到,HandlerExceptionResolver接口中定义了一个名为resolveException的方法,该方法主要用于处理Controller中的异常
        • 参数“Exception ex”即为Controller或其下层抛出的异常
        • 参数“Object handler”就是处理器适配器要执行的Handler对象
        • resolveException方法的返回值类型是ModelAndView,也就是说,可以通过这个返回值来设置发送异常时的显示页面
          • 典型的做法是转换成一个错误视图,或者返回某个HTTP状态码给请求端
          • 使用HandlerExceptionResolver实现全局异常处理器;当抛出异常后,要有相应的符合用户体验的友好界面显示异常
          • 综合实例:
          • (1)创建自定义异常类,名称为OperationException

            Spring-异常处理(6种讲解),第3张

            • 注意:
            • Spring管理的事务,无论是声明式事务还是注解式事务默认是在抛出运行异常(RuntimeException异常)时,才会被Spring框架捕获到然后回滚
            • 该自定义异常类继承的是RuntimeException类,因为一般项目的Service层逻辑都会使用Spring提供的事务管理,当Service层需要抛出自定义异常时,如果该自定义异常继承的是Exception类,则Spring提供的事务管理将会失效,所以这里的自定义异常类继承的是RuntimeException类,这样才不会使Spring提供的事务管理失效
            • (2)创建全局异常处理器OperationExceptionResolver类,该类实现HandlerExceptionResolver接口,并且实现resolveException方法

              Spring-异常处理(6种讲解),第4张

              • (3)创建名称为error.jsp的页面,符合用户体验的友好界面,用于显示异常信息
                • Spring MVC 为HandlerExceptionResolver提供了四个实现类:

                  (1)ExceptionHandlerExceptionResolver

                  • 该解析器是在 Spring 3.1 中引入的,并且在DispatcherServlet中默认启用
                  • 这实际上是前面介绍的@ExceptionHandler机制如何工作的核心组件
                  • 找到使用@ExceptionHandler注解的ServletInvocableHandlerMethod并调用,基于其执行结果构建ModelAndView

                    (2)SimpleMappingExceptionResolver

                    • 映射异常类名到视图名称,处理时使用此映射关系构建ModelAndView,或者使用缺省视图

                      (3)ResponseStatusExceptionResolver

                      • 这个解析器也在 Spring 3.0 中引入,并且在DispatcherServlet中默认启用
                      • 如果异常是ResponseStatusException或者使用了注解@ResponseStatus,则利用这些信息将异常翻译成HTTP状态字调用相应的sendError,返回空ModelAndView(与DefaultHandlerExceptionResolver相同,此解析器在处理响应正文的方式上受到限制 - 它确实将状态代码映射到响应上,但正文仍然为空)
                      • 这样的自定义异常可能如下所示:
                      • Spring-异常处理(6种讲解),第5张
                      • 注意:该异常解析器会递归检查异常的cause异常

                        (4)DefaultHandlerExceptionResolver

                        • 这个解析器是在 Spring 3.0 中引入的,并且在DispatcherServlet中默认启用
                        • Spring MVC缺省异常处理器,解析时将标准MVC异常翻译成HTTP状态字调用相应对象的方法#sendError,返回一个空ModelAndView对象(注意:不是null)
                        • 它用于将标准 Spring 异常解析为相应的 HTTP 状态代码,即客户端错误4xx和服务器错误5xx状态代码
                        • 虽然它确实正确设置了响应的状态代码,但有一个限制是它不会对响应正文设置任何内容
                        • 对于 REST API(状态代码实际上不足以向客户端提供足够的信息),响应还必须有一个正文,以允许应用程序提供有关失败的附加信息
                        • 这可以通过ModelAndView配置视图分辨率并渲染错误内容来解决,但该解决方案显然不是最优的
                        • 这就是为什么 Spring 3.2 引入了一个更好的选项,我们将在后面的部分中讨论

                          使用注解实现异常分类管理(@ControllerAdvice 和 @ExceptionHandler)

                          • 引言:
                          • 在开发中,我们会有如下的场景:某个接口中,存在一些业务异常
                          • 例如用户输入的参数校验失败、用户名密码不存在等
                          • 当触发这些业务异常时,我们需要抛出这些自定义的业务异常,并对其进行处理
                          • 一般我们要把这些异常信息的状态码和异常描述,友好地返回给调用者,调用者则利用状态码等信息判断异常的具体情况
                          • 过去,我们可能需要在 controller 层通过 try/catch 处理
                          • 首先 catch 自定义异常,然后 catch 其它异常
                          • 对于不同的异常,我们需要在 catch 的同时封装将要返回的对象
                          • 然而,这么做的弊端就是代码会变得冗长
                          • 每个接口都需要做 try/catch 处理,而且一旦需要调整,所有的接口都需要修改一遍,非常不利于代码的维护,如下段代码所示:

                            Spring-异常处理(6种讲解),第6张

                            • 那么,有没有什么方法可以简便地处理这些异常信息呢?
                            • 答案是肯定的
                            • Spring 3.2 中,新增了 @ControllerAdvice 注解,可以用于定义 @ExceptionHandler、@InitBinder、@ModelAttribute,并应用到所有 @RequestMapping 中
                            • @ControllerAdvice注解允许我们将之前的多个分散的@ExceptionHandler合并到一个全局错误处理组件中
                            • 简单来说就是,可以通过 @ControllerAdvice 注解配置一个全局异常处理类,来统一处理 controller 层中的异常,于此同时 controller 中可以不用再写 try/catch,这使得代码既整洁又便于维护
                              • 综合实例:定义自定义异常
                              • (1)自定义业务异常类

                                Spring-异常处理(6种讲解),第7张

                                • (2)@ControllerAdvice + @ExceptionHandler 配置全局异常处理类

                                  Spring-异常处理(6种讲解),第8张

                                  @ControllerAdvice

                                  • 类型:类注解;定义该类为全局异常处理类

                                    @ExceptionHandler

                                    • 类型:方法注解;定义该方法为异常处理方法
                                    • value 的值为需要处理的异常类的 class 文件
                                      • 这样,就可以对不同的异常进行统一处理了
                                      • 通常,为了使 controller 中不再使用任何 try/catch,也可以在 GlobalExceptionHandler 中对 Exception 做统一处理
                                      • 这样其他没有用 @ExceptionHandler 配置的异常就都会统一被处理
                                      • 遇到异常时抛出异常即可
                                      • 在业务中,遇到业务异常的地方,直接使用 throw 抛出对应的业务异常即可
                                      • 例如:

                                        Spring-异常处理(6种讲解),第9张


                                        注意:

                                        • 不一定必须在 controller 层本身抛出异常才能被 GlobalExceptionHandler 处理,只要异常最后是从 contoller 层抛出去的就可以被全局异常处理器处理
                                        • 异步方法中的异常不会被全局异常处理
                                        • 抛出的异常如果被代码内的 try/catch 捕获了,就不会被 GlobalExceptionHandler 处理了

                                          优点:

                                          • 减少代码冗余,代码便于维护

                                            缺点:

                                            • 只能处理 controller 层抛出的异常,对例如 Interceptor(拦截器)层的异常、定时任务中的异常、异步方法中的异常,不会进行处理

                                              使用 @ControllerAdvice 对不同的 Controller 分别捕获异常并处理

                                              • 引言
                                              • 上文介绍了如何在 SpringBoot 工程中对 Controller 配置全局异常处理
                                              • 后来在实际工程中,又有了如下需求:有些接口在发生异常时,需要持久化错误信息,而有的接口则不需要
                                              • 如果使用了全局异常处理,那每次发生了异常,还需要判断是哪个接口发生的异常,进而选择是否持久化错误日志
                                              • 那能不能对全局异常进行配置,对不同类型的接口使用不同的全局异常进行处理呢?
                                              • 经过查阅文档发现,Spring 提供了对 @ControllerAdvice 注解的配置,我们可以通过配置 @ControllerAdvice 指定拦截范围
                                              • @ControllerAdvice 指定 Controller 范围
                                              • 根据 API,我们可以看到注解 @ControllerAdvice 有如下几种配置:

                                                (1)basePackages

                                                Spring-异常处理(6种讲解),第10张

                                                • basePackages:指定一个或多个包,这些包及其子包下的所有 Controller 都被该 @ControllerAdvice 管理
                                                • 其中上面两种等价于 basePackages

                                                  (2)basePackageClasses

                                                  Spring-异常处理(6种讲解),第11张

                                                  • basePackageClasses:是 basePackages 的一种变形,指定一个或多个 Controller 类,这些类所属的包及其子包下的所有 Controller 都被该 @ControllerAdvice 管理

                                                    (3)assignableTypes

                                                    Spring-异常处理(6种讲解),第12张

                                                    • assignableTypes:指定一个或多个 Controller 类,这些类被该 @ControllerAdvice 管理

                                                      (4)annotations

                                                      Spring-异常处理(6种讲解),第13张

                                                      • annotations:指定一个或多个注解,被这些注解所标记的 Controller 会被该 @ControllerAdvice 管理
                                                        • 例子
                                                        • 这里用 assignableTypes 配置指定的 Controller 进行测试

                                                          (1)创建三个 Controller

                                                          • Spring-异常处理(6种讲解),第14张
                                                          • Spring-异常处理(6种讲解),第15张
                                                          • Spring-异常处理(6种讲解),第16张
                                                          • 其中,BusinessException 是自定义的异常类

                                                            (2)创建两个全局异常处理类

                                                            • Spring-异常处理(6种讲解),第17张
                                                            • Spring-异常处理(6种讲解),第18张

                                                              (3)分别调用接口,查看错误日志

                                                              • (1)调用 localhost:8080/test1

                                                              • 返回:GlobalExceptionHandler1 服务错误

                                                              • 即 MyController1 异常被 GlobalExceptionHandler1 全局异常类捕获

                                                              • (2)调用 localhost:8080/test2

                                                              • 返回:GlobalExceptionHandler2 服务错误

                                                              • 即 MyController2 异常被 GlobalExceptionHandler2 全局异常类捕获

                                                              • (3)调用 localhost:8080/test3

                                                              • 返回:

                                                              • Spring-异常处理(6种讲解),第19张

                                                              • 即 MyController3 异常没有被全局异常捕获

                                                                ResponseStatusException(Spring 5及以上版本)

                                                                • Spring 5 引入了ResponseStatusException类
                                                                • Spring-异常处理(6种讲解),第20张

                                                                  使用ResponseStatusException有什么好处?

                                                                  • 非常适合原型设计:我们可以非常快速地实施基本解决方案
                                                                  • 一种类型,多个状态代码:一种异常类型可以导致多种不同的响应;与@ExceptionHandler相比,这减少了紧密耦合
                                                                  • 我们不必创建那么多自定义异常类
                                                                  • 由于可以通过编程方式创建异常,因此我们可以更好地控制异常处理

                                                                    坏处:

                                                                    • 没有统一的异常处理方法:与提供全局处理方法的@ControllerAdvice相比,强制执行一些应用程序范围的约定更加困难
                                                                    • 代码重复:我们可能会发现自己在多个控制器中复制代码
                                                                      • 我们还应该注意到,可以在一个应用程序中组合不同的方法
                                                                      • 例如,我们可以全局实现 @ControllerAdvice,也可以在本地实现ResponseStatusException

                                                                        Spring Web MVC 中的异常处理流程

                                                                        • DispatcherServlet 类处理多个 HandlerExceptionResolver 对象
                                                                        • 提供了ExceptionHandlerExceptionResolver类、ResponseStatusExceptionResolver类和DefaultHandlerExceptionResolver类,这些类各司其职,处理异常
                                                                        • 其中 ExceptionHandlerExceptionResolver 类负责调用@ExceptionHandler注解的方法
                                                                        • DispatcherServlet(Spring 框架 5.1.9.RELEASE API)
                                                                        • 调度程序的异常解决策略可以通过 HandlerExceptionResolver 指定,例如将某些异常映射到错误页面
                                                                        • 默认为 ExceptionHandlerExceptionResolver、ResponseStatusExceptionResolver 和 DefaultHandlerExceptionResolver
                                                                        • 这些 HandlerExceptionResolver 可以通过应用程序上下文覆盖
                                                                        • HandlerExceptionResolver 可以给定任何 bean 名称(它们是按类型测试)
                                                                        • 查看Spring Web MVC的DispatcherServlet类的源代码:
                                                                        • Spring-异常处理(6种讲解),第21张
                                                                        • processHandlerException方法中,从this.handlerExceptionResolvers中---获取HandlerExceptionResolver对象,并调用resolveException方法
                                                                        • 每个对象的resolveException方法处理异常并返回一个ModelAndView对象
                                                                        • 查看 IntelliJ IDEA 调试器中正在处理的对象:
                                                                        • Spring-异常处理(6种讲解),第22张
                                                                        • Spring-异常处理(6种讲解),第23张
                                                                        • 可以看到HandlerExceptionResolverComposite对象管理着ExceptionHandlerExceptionResolver、ResponseStatusExceptionResolver和DefaultHandlerExceptionResolver
                                                                        • ExceptionHandlerExceptionResolver对象有一个名为ExceptionHandlerAdviceCache的实例变量,它包含带有@ControllerAdvice注解的类的对象
                                                                        • 如果查看ExceptionHandlerExceptionResolver的源码,可以看到它是注册在ExceptionHandlerAdviceCache中的
                                                                        • Spring-异常处理(6种讲解),第24张
                                                                        • Spring-异常处理(6种讲解),第25张