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




免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM