SpringMvc 全局异常捕获和处理实现方式总结
SpringMvc 网站在运行过程中,任何地方都可能会出现异常。捕获异常并记录日志是一个非常重要的发现问题和排查问题的途径。我们可以预见到某些代码可能会出现异常,但是还有很多情况下的异常是无法预见到的。因此如果能够全局捕获异常并统一进行异常处理,将是一个最佳的解决方案。
SpringMvc 提供了两种全局异常捕获和处理的实现方式,一种是实现接口 HandlerExceptionResolver 的方式,一种是采用注解 @ControllerAdvice 的实现方式。在实际开发过程中绝大部分情况下采用纯注解的实现方式。本篇博客将从代码层面演示这两种全局异常捕获和处理的实现方式,并在博客的最后提供 Demo 源代码的下载。
一、搭建工程
新建一个 maven 项目,导入相关 jar 包,我所导入的 jar 包都是最新的,内容如下:
有关具体的 jar 包地址,可以在 https://mvnrepository.com 上进行查询。
<dependencies>
<!--导入 servlet 相关的 jar 包-->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>jsp-api</artifactId>
<version>2.2</version>
<scope>provided</scope>
</dependency>
<!--导入 Spring 核心 jar 包-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.18</version>
</dependency>
<!--导入 SpringMvc 的 jar 包-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.3.18</version>
</dependency>
<!--导入 jackson 相关的 jar 包-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.13.1</version>
</dependency>
</dependencies>
配置好引用的 jar 包后,打开右侧的 Maven 窗口,刷新一下,这样 Maven 会自动下载所需的 jar 包文件。
搭建好的项目工程整体目录比较简单,具体如下图所示:
com.jobs.config 包下存储的是 SpringMvc 的配置文件和 Servlet 的初始化文件
com.jobs.controller 包下存储的是用于提供 api 接口的类
com.jobs.domain 包下存储的是 JavaBean 实体类
web 目录下放置的是网站文件,只有一个静态页面和一些 js 文件
有关 SpringMvc 注解配置相关的内容,跟之前发布的博客相比,重复性内容太多了,因此这里就不再发布了。
二、通过实现接口的方式实现全局异常捕获处理
新建一个类,只要实现了 HandlerExceptionResolver 接口即可,然后在该类上增加 @Component 注解,让 SpringMvc 装载到容器内即可实现全局异常捕获。在本 Demo 中的具体代码如下所示:
package com.jobs.exception;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/*
只要在实现了 HandlerExceptionResolver 接口的类上,增加了 @Component 注解,
能够让 SpringMvc 装载,那么当出现相应类型的异常后,就会自动捕获执行该类的相应的方法处理
需要注意的是:实现了 HandlerExceptionResolver 接口的异常处理类,加载的比较晚,
在 Controller 接收完参数后,才会进行异常监控,
所以当 Controller 接收参数中出现问题时(比如类型转换错误),这里是监控不到的,
因此在实际开发中,很少使用这种全局异常处理方案。
比较不爽的是:这里要返回一个 ModelAndView ,也就是需要跳转到一个页面,因此不够灵活
*/
//此处的 ExceptionResolver 和 ExceptionAdvice 的 @Component 注解不要同时开启
//@Component
public class ExceptionResolver implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) {
System.out.println("捕获到了异常:" + ex);
//根据异常类型,确定异常处理方式
ModelAndView mv = new ModelAndView();
if (ex instanceof NullPointerException) {
mv.addObject("msg", "发生了空指针异常");
} else if (ex instanceof ArithmeticException) {
mv.addObject("msg", "发生了算数运算异常");
} else {
mv.addObject("msg", ex.getMessage());
}
mv.setViewName("error");
return mv;
}
}
这种全局异常捕获和处理的实现方式,属于早期的实现方式,返回值为 ModelAndView ,意味着当发生异常时需要跳转到一个页面中,不够灵活。因为目前比较流行前后端分离的网站开发方式,后端只是提供接口,当出现异常时想直接返回错误状态和相关信息。这里模拟了通过系统内置的异常类型判断,确定异常处理方式。需要注意的是:这种实现方式无法捕获到 Controller 中的方法接收参数时,发生参数类型转换异常的情况。
用来测试的 TestController1 实现内容为:
package com.jobs.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@RequestMapping("/test1")
@Controller
public class TestController1 {
//这个可以正常访问
@RequestMapping("/req1")
public String successRequest() {
System.out.println("请求了 /test1/req1 ,访问正常....跳转到 success.jsp 页面");
return "success";
}
//会发生除以0的算术异常
@RequestMapping("/req2")
public String zoroExceptionRequest() {
System.out.println("请求了 /test1/req2 ,出现算术异常....跳转到 error.jsp 页面");
//这里的异常会被 ExceptionResolver 异常处理类捕获到
int a = 1 / 0;
return "success";
}
//会发生空指针异常
@RequestMapping("/req3")
public String nullExceptionRequest() {
System.out.println("请求了 /test1/req3 ,出现空指针异常....跳转到 error.jsp 页面");
//这里的异常会被 ExceptionResolver 异常处理类捕获到
String str = null;
int len = str.length();
return "success";
}
//会发生数组越界异常
@RequestMapping("/req4")
public String otherExceptionRequest() {
System.out.println("请求了 /test1/req4 ,出现其它类型的异常....跳转到 error.jsp 页面");
//这里的异常会被 ExceptionResolver 异常处理类捕获到,作为其它类型的异常处理
int[] arr = new int[]{1, 2, 3};
int num = arr[100];
return "success";
}
//无法捕获到参数类型转换异常,这里请传入一个非数字的字符串
//比如 http://localhost:8080/test1/req5?age=aaa
@RequestMapping("/req5")
public String paramException(int age)
{
//实现了 ExceptionResolver 接口的异常处理类,无法捕获参数类型转换异常
System.out.println("请求了 /test1/req5 ,如果传入的 age 参数无法转换为数字,将出异常");
return "success";
}
}
我们访问最后一个接口发现,这种全局异常捕获和处理的实现方式,确实无法捕获传入的参数类型转换异常问题。
三、通过 特定注解的方式实现全局异常捕获处理
新建一个类作为全局异常捕获和处理类,在类上面增加 @ControllerAdvice 即可,在类里面可以定义相关的方法,在方法上可以通过注解 @ExceptionHandler 标明要捕获和处理的异常类型,在实际场景中可以仅定义一个异常处理方法,标明要处理的异常类型为 Exception.class 即可,那么意味着捕获和处理所有的异常。本 Demo 为了演示功能,所以定义了两种自定义的异常类型,分别进行捕获和处理。
package com.jobs.exception;
import com.jobs.domain.MyResult;
import com.jobs.exception.myException.BLLException;
import com.jobs.exception.myException.DALException;
import org.springframework.stereotype.Component;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
//此处的 ExceptionAdvice 和 ExceptionResolver 的 @Component 注解不要同时开启
@Component
//使用 @ControllerAdvice 注解,实现对异常分类处理
@ControllerAdvice
public class ExceptionAdvice {
//----------以跳转页面的方式处理-----------
//使用 @ExceptionHandler 注解,标明对哪种类别的异常进行处理
@ExceptionHandler(BLLException.class)
public String handBLLException( Exception ex, Model m) {
//将异常信息,记录到日志中
System.out.println("BLL中的方法发生异常:" + ex);
//给用户展示友好的信息,隐藏具体的问题细节
m.addAttribute("msg", "很抱歉,网站出现问题,请稍后再访问");
return "error";
}
@ExceptionHandler(DALException.class)
public String handDALException(Exception ex, Model m) {
//将异常信息,记录到日志中
System.out.println("DAL中的方法发生异常:" + ex);
//给用户展示友好的信息,隐藏具体的问题细节
m.addAttribute("msg", "网站出现问题,请联系客户进行处理");
return "error";
}
@ExceptionHandler(Exception.class)
public String handException(Exception ex, Model m) {
//将异常信息,记录到日志中
System.out.println("其它地方发生异常:" + ex);
//给用户展示友好的信息,隐藏具体的问题细节
m.addAttribute("msg", "很抱歉,发生了问题,请联系管理员");
return "error";
}
}
本 Demo 中自定义了两种类型的异常:BLLException 和 DALException ,可以分别用来包装业务层抛出的异常和数据访问层抛出的异常。自定义异常的方式非常简单,继承 RuntimeException 类重写其所有构造方法即可,具体内容如下:
package com.jobs.exception.myException;
//自定义业务层异常类,覆盖父类所有的构造方法
public class BLLException extends RuntimeException{
public BLLException() {
}
public BLLException(String message) {
super(message);
}
public BLLException(String message, Throwable cause) {
super(message, cause);
}
public BLLException(Throwable cause) {
super(cause);
}
public BLLException(String message, Throwable cause,
boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
package com.jobs.exception.myException;
//自定义数据访问层异常类,覆盖父类所有的构造方法
public class DALException extends RuntimeException{
public DALException() {
}
public DALException(String message) {
super(message);
}
public DALException(String message, Throwable cause) {
super(message, cause);
}
public DALException(Throwable cause) {
super(cause);
}
public DALException(String message, Throwable cause,
boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
用来测试的 TestController2 的具体内容如下:
package com.jobs.controller;
import com.jobs.exception.myException.BLLException;
import com.jobs.exception.myException.DALException;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
//把相关的异常,包装成自己预先定义的异常,这样异常种类数量就可控了,可以分别处理
@RequestMapping("/test2")
@Controller
public class TestController2 {
//这个可以正常访问
@RequestMapping("/req1")
public String successRequest() {
System.out.println("请求了 /test2/req1 ,访问正常....跳转到 success.jsp 页面");
return "success";
}
//会发生除以0的算术异常
@RequestMapping("/req2")
public String zoroExceptionRequest() {
System.out.println("请求了 /test2/req2 ,出现算术异常....跳转到 error.jsp 页面");
try {
//这里的异常会被 ExceptionResolver 异常处理类捕获到
int a = 1 / 0;
}
catch (Exception ex)
{
//这里把异常包装成 BLLException 进行抛出
throw new BLLException(ex);
}
return "success";
}
//会发生空指针异常
@RequestMapping("/req3")
public String nullExceptionRequest() {
System.out.println("请求了 /test2/req3 ,出现空指针异常....跳转到 error.jsp 页面");
try {
//这里的异常会被 ExceptionResolver 异常处理类捕获到
String str = null;
int len = str.length();
}
catch (Exception ex)
{
//这里把异常包装成 DALException 进行抛出
throw new DALException(ex);
}
return "success";
}
//会发生数组越界异常
@RequestMapping("/req4")
public String otherExceptionRequest() {
System.out.println("请求了 /test2/req4 ,出现其它类型的异常....跳转到 error.jsp 页面");
//这里的异常会被 ExceptionAdvice 异常处理类捕获到,作为其它类型的异常处理
int[] arr = new int[]{1, 2, 3};
int num = arr[100];
return "success";
}
//无法捕获到参数类型转换异常,这里请传入一个非数字的字符串
//比如 http://localhost:8080/test2/req5?age=aaa
@RequestMapping("/req5")
public String paramException(int age)
{
//ExceptionAdvice 可以捕获参数类型转换异常
System.out.println("请求了 /test2/req5 ,如果传入的 age 参数无法转换为数字,将出异常");
return "success";
}
}
访问第 2 个方法会抛出 BLLException,被 ExceptionAdvice 异常处理类的 handBLLException 捕获;访问第 3 个方法会抛出 DALException,被 ExceptionAdvice 异常处理类的 handDALException 捕获;访问第 4 和第 5 个方法会抛出 Java 内置的具体异常,被 ExceptionAdvice 异常处理类的 handException 捕获;其中访问第 5 个方法时,传入错误的参数数据类型,抛出的异常也能被捕获到。这样就完美的实现了所有异常的捕获和处理。
到此为止,两种全局异常捕获和处理的实现方式,已经介绍完毕。第一种实现方式仅作为了解。强烈推进大家实现第二种实现方式。
需要注意的是:如果下载了本博客的 Demo 进行测试时,两种全局异常捕获实现类上的 @Component 注解不要都启用,测试哪种方式就启用哪个实现类上的 @Component 注解,注释掉另外一个实现类上的注解。
本博客的 Demo 源代码下载地址为:https://files.cnblogs.com/files/blogs/699532/SpringMvc_Exception.zip