⭐表示重要。

第一章:概念

1.1 微观

  • 将异常类型和某一个具体的视图关联起来,建立一个映射关系。好处是可以通过 SpringMVC 框架帮助我们管理异常。

  • 声明式管理异常:在配置文件中指定异常类型和视图之间的对应关系,在配置文件或配置类中统一管理

  • 编程式管理异常:需要我们自己手动 try … catch … 捕获异常,然后再手动跳转到某个页面。

1.2 宏观

  • 整个项目从架构这个层面设计的异常处理的统一机制和规范。

  • 一个项目中会包含很多模块,各个模块需要分工完成。如果张三负责的模块按照 A 方案处理异常,李四负责的模块按照 B 方案处理异常……各个模块处理异常的思路、代码、命名细节都不一样,那么就会让整个项目非常混乱。

第二章:异常映射的好处

  • 使用声明式管理异常代替编程式管理异常,可以让异常控制和核心业务解耦,二者各自维护,结构性更好。
  • 整个项目层面使用同一套规则来管理异常,可以让整个项目代码风格更加统一、简洁,也便于团队成员之间的彼此协作。

第三章:准备工作

3.1 环境准备

  • JDK 11+。
  • IDEA 2021+。
  • Maven 3.8。

3.2 导入依赖

  • pom.xml
  1. <!-- SpringMVC -->
  2. <dependency>
  3. <groupId>org.springframework</groupId>
  4. <artifactId>spring-webmvc</artifactId>
  5. <version>5.3.12</version>
  6. </dependency>
  7. <!-- 日志 -->
  8. <dependency>
  9. <groupId>ch.qos.logback</groupId>
  10. <artifactId>logback-classic</artifactId>
  11. <version>1.2.6</version>
  12. </dependency>
  13. <!-- ServletAPI -->
  14. <dependency>
  15. <groupId>javax.servlet</groupId>
  16. <artifactId>javax.servlet-api</artifactId>
  17. <version>4.0.1</version>
  18. <scope>provided</scope>
  19. </dependency>
  20. <!-- Spring5和Thymeleaf整合包 -->
  21. <dependency>
  22. <groupId>org.thymeleaf</groupId>
  23. <artifactId>thymeleaf-spring5</artifactId>
  24. <version>3.0.12.RELEASE</version>
  25. </dependency>
  26. <dependency>
  27. <groupId>com.fasterxml.jackson.core</groupId>
  28. <artifactId>jackson-databind</artifactId>
  29. <version>2.13.0</version>
  30. </dependency>

3.3 web.xml

  • web.xml
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  3. xmlns="http://xmlns.jcp.org/xml/ns/javaee"
  4. xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
  5. http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
  6. version="4.0">
  7. <!-- 配置过滤器解决 POST 请求的字符乱码问题 -->
  8. <filter>
  9. <filter-name>CharacterEncodingFilter</filter-name>
  10. <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
  11. <!-- encoding参数指定要使用的字符集名称 -->
  12. <init-param>
  13. <param-name>encoding</param-name>
  14. <param-value>UTF-8</param-value>
  15. </init-param>
  16. <!-- 请求强制编码 -->
  17. <init-param>
  18. <param-name>forceRequestEncoding</param-name>
  19. <param-value>true</param-value>
  20. </init-param>
  21. <!-- 响应强制编码 -->
  22. <init-param>
  23. <param-name>forceResponseEncoding</param-name>
  24. <param-value>true</param-value>
  25. </init-param>
  26. </filter>
  27. <filter-mapping>
  28. <filter-name>CharacterEncodingFilter</filter-name>
  29. <url-pattern>/*</url-pattern>
  30. </filter-mapping>
  31. <!-- 配置HiddenHttpMethodFilter -->
  32. <filter>
  33. <filter-name>hiddenHttpMethodFilter</filter-name>
  34. <filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
  35. </filter>
  36. <filter-mapping>
  37. <filter-name>hiddenHttpMethodFilter</filter-name>
  38. <url-pattern>/*</url-pattern>
  39. </filter-mapping>
  40. <!-- 配置SpringMVC中负责处理请求的核心Servlet,也被称为SpringMVC的前端控制器 -->
  41. <servlet>
  42. <servlet-name>dispatcherServlet</servlet-name>
  43. <!-- DispatcherServlet的全类名 -->
  44. <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
  45. <!-- 通过初始化参数指定SpringMVC配置文件位置 -->
  46. <init-param>
  47. <!-- 如果不记得contextConfigLocation配置项的名称,可以到DispatcherServlet的父类FrameworkServlet中查找 -->
  48. <param-name>contextConfigLocation</param-name>
  49. <!-- 使用classpath:说明这个路径从类路径的根目录开始才查找 -->
  50. <param-value>classpath:springmvc.xml</param-value>
  51. </init-param>
  52. <!-- 作为框架的核心组件,在启动过程中有大量的初始化操作要做,这些操作放在第一次请求时才执行非常不恰当 -->
  53. <!-- 我们应该将DispatcherServlet设置为随Web应用一起启动 -->
  54. <load-on-startup>1</load-on-startup>
  55. </servlet>
  56. <servlet-mapping>
  57. <servlet-name>dispatcherServlet</servlet-name>
  58. <!-- 对DispatcherServlet来说,url-pattern有两种方式配置 -->
  59. <!-- 方式一:配置“/”,表示匹配整个Web应用范围内所有请求。这里有一个硬性规定:不能写成“/*”。只有这一个地方有这个特殊要求,以后我们再配置Filter还是可以正常写“/*”。 -->
  60. <!-- 方式二:配置“*.扩展名”,表示匹配整个Web应用范围内部分请求 -->
  61. <url-pattern>/</url-pattern>
  62. </servlet-mapping>
  63. </web-app>

3.4 日志文件

  • logback.xml
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <configuration debug="true">
  3. <!-- 指定日志输出的位置 -->
  4. <appender name="STDOUT"
  5. class="ch.qos.logback.core.ConsoleAppender">
  6. <encoder>
  7. <!-- 日志输出的格式 -->
  8. <!-- 按照顺序分别是:时间、日志级别、线程名称、打印日志的类、日志主体内容、换行 -->
  9. <pattern>[%d{HH:mm:ss.SSS}] [%-5level] [%thread] [%logger] [%msg]%n</pattern>
  10. <charset>UTF-8</charset>
  11. </encoder>
  12. </appender>
  13. <!-- 设置全局日志级别。日志级别按顺序分别是:DEBUG、INFO、WARN、ERROR -->
  14. <!-- 指定任何一个日志级别都只打印当前级别和后面级别的日志。 -->
  15. <root level="INFO">
  16. <!-- 指定打印日志的appender,这里通过“STDOUT”引用了前面配置的appender -->
  17. <appender-ref ref="STDOUT" />
  18. </root>
  19. <!-- 根据特殊需求指定局部日志级别 -->
  20. <logger name="org.springframework.web.servlet.DispatcherServlet" level="DEBUG" />
  21. </configuration>

3.5 SpringMVC 配置文件

  • springmvc.xml
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  3. xmlns:context="http://www.springframework.org/schema/context"
  4. xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns="http://www.springframework.org/schema/beans"
  5. xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd">
  6. <!-- 自动扫描包 -->
  7. <context:component-scan base-package="com.github.fairy.era.mvc.handler"></context:component-scan>
  8. <!-- 配置视图解析器 -->
  9. <bean id="viewResolver" class="org.thymeleaf.spring5.view.ThymeleafViewResolver">
  10. <property name="order" value="1"/>
  11. <property name="characterEncoding" value="UTF-8"/>
  12. <property name="templateEngine">
  13. <bean class="org.thymeleaf.spring5.SpringTemplateEngine">
  14. <property name="templateResolver">
  15. <bean class="org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver">
  16. <!-- 物理视图:视图前缀+逻辑视图+视图后缀 -->
  17. <!-- 视图前缀 -->
  18. <property name="prefix" value="/WEB-INF/templates/"/>
  19. <!-- 视图后缀 -->
  20. <property name="suffix" value=".html"/>
  21. <property name="templateMode" value="HTML5"/>
  22. <property name="characterEncoding" value="UTF-8"/>
  23. </bean>
  24. </property>
  25. </bean>
  26. </property>
  27. </bean>
  28. <mvc:annotation-driven/>
  29. <mvc:default-servlet-handler/>
  30. <mvc:view-controller path="/" view-name="portal"/>
  31. </beans>

第四章:基于 XML 的异常映射

4.1 表单

  • 示例:
  1. <!DOCTYPE html>
  2. <html lang="en" xmlns:th="http://www.thymeleaf.org">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>首页</title>
  6. </head>
  7. <body>
  8. <a th:href="@{/throw/arithmetic/exception}">数学异常</a>
  9. </body>
  10. </html>

4.2 handler 方法

  • 示例:
  1. package com.github.fairy.era.mvc.handler;
  2. import org.slf4j.Logger;
  3. import org.slf4j.LoggerFactory;
  4. import org.springframework.stereotype.Controller;
  5. import org.springframework.web.bind.annotation.GetMapping;
  6. /**
  7. * @author 许大仙
  8. * @version 1.0
  9. * @since 2021-11-11 14:58
  10. */
  11. @Controller
  12. public class DemoHandler {
  13. private final Logger logger = LoggerFactory.getLogger(this.getClass());
  14. @GetMapping("/throw/arithmetic/exception")
  15. public String throwArithmeticException() {
  16. int i = 10 / 0;
  17. return "target";
  18. }
  19. }

4.3 配置异常映射

  • 在 springmvc.xml 中配置异常映射之后,SpringMVC 会根据异常映射的信息,在捕获到指定的异常对象后,将异常对象存入到请求域汇总,然后转发和异常类型关联的视图。

  • 示例:

  • springmvc.xml
  1. <!-- 异常映射 -->
  2. <bean id="exceptionResolver" class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
  3. <!-- 配置异常映射管理 -->
  4. <property name="exceptionMappings">
  5. <props>
  6. <!-- key属性:指定异常类型 -->
  7. <!-- 文本标签体:和异常类型对应的逻辑视图 -->
  8. <prop key="java.lang.ArithmeticException">error-arithmetic</prop>
  9. <prop key="java.lang.RuntimeException">error-runtime</prop>
  10. </props>
  11. </property>
  12. <!-- 使用 exceptionAttribute 属性配置将异常对象存入请求域时使用的属性名 -->
  13. <!-- 默认是 exception -->
  14. <property name="exceptionAttribute" value="arithmetic"/>
  15. </bean>
  • 完整的 springmvc.xml
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  3. xmlns:context="http://www.springframework.org/schema/context"
  4. xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns="http://www.springframework.org/schema/beans"
  5. xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd">
  6. <!-- 自动扫描包 -->
  7. <context:component-scan base-package="com.github.fairy.era.mvc.handler"></context:component-scan>
  8. <!-- 配置视图解析器 -->
  9. <bean id="viewResolver" class="org.thymeleaf.spring5.view.ThymeleafViewResolver">
  10. <property name="order" value="1"/>
  11. <property name="characterEncoding" value="UTF-8"/>
  12. <property name="templateEngine">
  13. <bean class="org.thymeleaf.spring5.SpringTemplateEngine">
  14. <property name="templateResolver">
  15. <bean class="org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver">
  16. <!-- 物理视图:视图前缀+逻辑视图+视图后缀 -->
  17. <!-- 视图前缀 -->
  18. <property name="prefix" value="/WEB-INF/templates/"/>
  19. <!-- 视图后缀 -->
  20. <property name="suffix" value=".html"/>
  21. <property name="templateMode" value="HTML5"/>
  22. <property name="characterEncoding" value="UTF-8"/>
  23. </bean>
  24. </property>
  25. </bean>
  26. </property>
  27. </bean>
  28. <mvc:annotation-driven/>
  29. <mvc:default-servlet-handler/>
  30. <mvc:view-controller path="/" view-name="portal"/>
  31. <!-- 异常映射 -->
  32. <bean id="exceptionResolver" class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
  33. <!-- 配置异常映射管理 -->
  34. <property name="exceptionMappings">
  35. <props>
  36. <!-- key属性:指定异常类型 -->
  37. <!-- 文本标签体:和异常类型对应的逻辑视图 -->
  38. <prop key="java.lang.ArithmeticException">error-arithmetic</prop>
  39. <prop key="java.lang.RuntimeException">error-runtime</prop>
  40. </props>
  41. </property>
  42. <!-- 使用 exceptionAttribute 属性配置将异常对象存入请求域时使用的属性名 -->
  43. <!-- 默认是 exception -->
  44. <property name="exceptionAttribute" value="arithmetic"/>
  45. </bean>
  46. </beans>

4.4 异常范围

  • 如果在配置文件中,发现多个匹配的异常类型,那么 SpringMVC 会采纳范围上最接近的异常映射关系。

  • 示例:springmvc.xml

  1. <!-- 异常映射 -->
  2. <bean id="exceptionResolver" class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
  3. <!-- 配置异常映射管理 -->
  4. <property name="exceptionMappings">
  5. <props>
  6. <!-- key属性:指定异常类型 -->
  7. <!-- 文本标签体:和异常类型对应的逻辑视图 -->
  8. <!-- 如果发生的是ArithmeticException异常,那么会跳转到error-arithmetic -->
  9. <prop key="java.lang.ArithmeticException">error-arithmetic</prop>
  10. <prop key="java.lang.RuntimeException">error-runtime</prop>
  11. </props>
  12. </property>
  13. <!-- 使用 exceptionAttribute 属性配置将异常对象存入请求域时使用的属性名 -->
  14. <!-- 默认是 exception -->
  15. <property name="exceptionAttribute" value="arithmetic"/>
  16. </bean>

4.5 异常视图页面

  • error-arithmetic.html
  1. <!DOCTYPE html>
  2. <html lang="en" xmlns:th="http://www.thymeleaf.org">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>错误页面</title>
  6. </head>
  7. <body>
  8. <p th:text="${arithmetic}">异常信息</p>
  9. <p th:text="${arithmetic.message}">异常信息</p>
  10. </body>
  11. </html>

第五章:基于注解的异常映射(⭐)

5.1 创建异常处理器类

  • 使用 @ControllerAdvice 注解标注异常处理器类。

创建异常处理器类.png

  • 示例:
  1. package com.github.fairy.era.mvc.exception;
  2. import org.springframework.ui.Model;
  3. import org.springframework.web.bind.annotation.ControllerAdvice;
  4. import org.springframework.web.bind.annotation.ExceptionHandler;
  5. /**
  6. * @author 许大仙
  7. * @version 1.0
  8. * @since 2021-11-16 17:03
  9. */
  10. @ControllerAdvice // 使用 @ControllerAdvice 注解标注异常处理器类
  11. public class ExceptionHandlerAdvice {
  12. }

5.2 将异常处理器类加入到 IOC 容器中

  • springmvc.xml
  1. <context:component-scan base-package="com.github.fairy.era.mvc"></context:component-scan>
  • 完整的 springmvc.xml
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  3. xmlns:context="http://www.springframework.org/schema/context"
  4. xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns="http://www.springframework.org/schema/beans"
  5. xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd">
  6. <!-- 自动扫描包 -->
  7. <context:component-scan base-package="com.github.fairy.era.mvc"></context:component-scan>
  8. <!-- 配置视图解析器 -->
  9. <bean id="viewResolver" class="org.thymeleaf.spring5.view.ThymeleafViewResolver">
  10. <property name="order" value="1"/>
  11. <property name="characterEncoding" value="UTF-8"/>
  12. <property name="templateEngine">
  13. <bean class="org.thymeleaf.spring5.SpringTemplateEngine">
  14. <property name="templateResolver">
  15. <bean class="org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver">
  16. <!-- 物理视图:视图前缀+逻辑视图+视图后缀 -->
  17. <!-- 视图前缀 -->
  18. <property name="prefix" value="/WEB-INF/templates/"/>
  19. <!-- 视图后缀 -->
  20. <property name="suffix" value=".html"/>
  21. <property name="templateMode" value="HTML5"/>
  22. <property name="characterEncoding" value="UTF-8"/>
  23. </bean>
  24. </property>
  25. </bean>
  26. </property>
  27. </bean>
  28. <mvc:annotation-driven/>
  29. <mvc:default-servlet-handler/>
  30. <mvc:view-controller path="/" view-name="portal"/>
  31. </beans>

5.3 声明处理异常的方法

  • 使用 @ExceptionHandler 注解标记异常处理方法。

  • 示例:

  1. package com.github.fairy.era.mvc.exception;
  2. import org.springframework.ui.Model;
  3. import org.springframework.web.bind.annotation.ControllerAdvice;
  4. import org.springframework.web.bind.annotation.ExceptionHandler;
  5. /**
  6. * @author 许大仙
  7. * @version 1.0
  8. * @since 2021-11-16 17:03
  9. */
  10. @ControllerAdvice // 使用 @ControllerAdvice 注解标注异常处理器类
  11. public class ExceptionHandlerAdvice {
  12. // @ExceptionHandler注解:标记异常处理方法
  13. // value属性:指定匹配的异常类型
  14. // 异常类型的形参:SpringMVC 捕获到的异常对象
  15. @ExceptionHandler(ArithmeticException.class)
  16. public String resolveArithmeticException(Exception exception, Model model) {
  17. // 我们可以自己手动将异常对象存入模型
  18. model.addAttribute("arithmetic", exception);
  19. // 返回逻辑视图的名称
  20. return "error-arithmetic";
  21. }
  22. }

注意:当一个异常类型在基于 XML和注解的配置中都能找到对应的映射,以注解为准。

第六章:区分请求类型

6.1 分析问题

  • 异常处理机制和拦截器机制都面临这样的问题:

区分请求类型之分析问题.png

6.2 判断依据

  • 查看请求消息头中是否包含 Ajax 请求独有的特征:
    • Accept 请求消息头:包含 application/json 。
    • X-Requested-With 请求消息头:包含 XMLHttpRequest 。
  • 两个条件满足一个即可。

  • 示例:

  1. package com.github.fairy.era.mvc.util;
  2. import javax.servlet.http.HttpServletRequest;
  3. /**
  4. * @author 许大仙
  5. * @version 1.0
  6. * @since 2021-11-16 21:04
  7. */
  8. public class MVCUtil {
  9. /**
  10. * 判断当前请求是否为Ajax请求
  11. *
  12. * @param request 请求对象
  13. * @return true:当前请求是Ajax请求
  14. * false:当前请求不是Ajax请求
  15. */
  16. public static boolean judgeRequestType(HttpServletRequest request) {
  17. // 1.获取请求消息头
  18. String acceptHeader = request.getHeader("Accept");
  19. String xRequestHeader = request.getHeader("X-Requested-With");
  20. // 2.判断
  21. return (acceptHeader != null && acceptHeader.contains("application/json")) || (xRequestHeader != null && xRequestHeader.equals("XMLHttpRequest"));
  22. }
  23. }

6.3 兼容两种请求的处理方法

  • 示例:
  1. package com.github.fairy.era.mvc.exception;
  2. import com.github.fairy.era.mvc.util.MVCUtil;
  3. import org.springframework.web.bind.annotation.ControllerAdvice;
  4. import org.springframework.web.bind.annotation.ExceptionHandler;
  5. import javax.servlet.http.HttpServletRequest;
  6. import javax.servlet.http.HttpServletResponse;
  7. import java.io.IOException;
  8. /**
  9. * @author 许大仙
  10. * @version 1.0
  11. * @since 2021-11-16 21:05
  12. */
  13. @ControllerAdvice
  14. public class ExceptionHandlerAdvice {
  15. @ExceptionHandler(value = Exception.class)
  16. public String resolveException(Exception e, HttpServletRequest request, HttpServletResponse response) throws IOException {
  17. // 调用工具方法判断当前请求是否是 Ajax 请求
  18. boolean judgeResult = MVCUtil.judgeRequestType(request);
  19. if (judgeResult) {
  20. // 对 Ajax 请求返回字符串作为响应体
  21. String message = e.getMessage();
  22. response.setContentType("text/html;charset=UTF-8");
  23. response.getWriter().write(message);
  24. // 上面已经使用原生 response 对象返回了响应,这里就不返回视图名称了
  25. return null;
  26. }
  27. // 对普通请求返回逻辑视图名称
  28. return "error-exception";
  29. }
  30. }