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 包文件。

搭建好的项目工程整体目录比较简单,具体如下图所示:

image

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



posted @ 2022-04-16 19:06  乔京飞  阅读(10060)  评论(0编辑  收藏  举报