Spring Boot 全局異常處理


說到異常處理,我們都知道使用 try-catch 可以捕捉異常,可以 throws 拋出異常。那么在 Spring Boot 中我們如何處理異常,如何是的處理更加優雅,如何全局處理異常。是本章討論解決的問題。

首先讓我們簡單了解或重新學習下 Java 的異常機制。

本項目源碼下載

1 Java 異常機制概述

Spring Boot 的所有異常處理都基於 java 的。

1.1 Java 異常類圖

Java 異常類圖

  1. Java 內部的異常類 Throwable 包括了 ExceptionError 兩大類,所有的異常類都是 Object 對象。
  2. Error 是不可捕捉的異常,通俗的理解就是由於 java 內部 jvm 引起的不可預見的異常,比如 java 虛擬機運行錯誤,當內存資源錯誤,將會出現 OutOfMemoryError。此時 java 虛擬機會選擇終止線程。
  3. Excetpion 異常是程序本身引起的,它又分為運行時異常 RuntimeException,和非運行時(編譯時)IOException 等異常。
  4. 運行時異常 RuntimeException 例如:除數為零,將引發 ArrayIndexOutOfBoundException 異常。
  5. 非運行異常都是可查可捕捉的。Java 編譯器會告訴程序他錯了,錯在哪里,正確的建議什么。我們可以通過 throws 配合 try-catch 來處理。

1.2 Exception 運行時異常和編譯異常

  1. 運行時異常 即 RuntimeException 類型下的異常
  2. 編譯異常 即 Exception 類型下除了 RuntimeException 類型的異常,例如 IOException

1.3 可查異常與不可查異常

  1. 可查異常 即 Exception 類型下除了 RuntimeException 類型的異常,都是可查的,具有可查性。
  2. 不可查異常 錯誤類 ErrorRuntimeException 類型的異常都是不可查的,具有不可查性。

2 Java 異常處理機制

2.1 異常處理機制的分類

在 Java 應用程序中,異常處理機制為:拋出異常捕捉異常

  • 拋出異常:當一個方法出現錯誤引發異常時,方法創建異常對象並交付運行時系統,異常對象中包含了異常類型和異常出現時的程序狀態等異常信息。運行時系統負責尋找處置異常的代碼並執行。

  • 捕獲異常:在方法拋出異常之后,運行時系統將轉為尋找合適的異常處理器(exception handler)。潛在的異常處理器是異常發生時依次存留在調用棧中的方法的集合。當異常處理器所能處理的異常類型與方法拋出的異常類型相符時,即為合適 的異常處理器。運行時系統從發生異常的方法開始,依次回查調用棧中的方法,直至找到含有合適異常處理器的方法並執行。當運行時系統遍歷調用棧而未找到合適 的異常處理器,則運行時系統終止。同時,意味着Java程序的終止。

針對不同的異常類型,Java 對處理的要求不一樣

  1. Error 錯誤,由於不可捕捉,不可查詢,Java 允許不做任何處理。
  2. 對於運行時異常 RuntimeException 不可查詢異常,Java 允許程序忽略運行時異常,Java 系統會自動記錄並處理。
  3. 對於所有可查異常都可捕捉,Java 自己。

2.2 捕獲異常 try、catch、finally

注意 finally 不論程序如何執行都會執行到

try{
    //可能出現異常的業務代碼
}catch(Exception1 e1){
    //異常處理1
}catch(Exception2 e2){
    //異常處理2
}catch(Exceptionn en){
    //異常處理n...
}
finally{
    //無論是否是否異常都會執行的地方
}

2.2.1 try、catch 流程規則

trycatch 語句,try只有一個,catch 可以有多個,也就是當有多個異常的時候,不需要編寫多個 try-catch 模塊,只要寫一個 try 多個 catch 就可以。

try{
    //可能出現異常的業務代碼
}catch(Exception1 e1){
    //異常處理1
}catch(Exception2 e2){
    //異常處理2
}catch(Exceptionn en){
    //異常處理n...
}

2.2.2 try、catch 、finally

trycatchfinally 語句中,finally 並不是必須的,但在有的場景確是非常實用的。

try{
    //可能出現異常的業務代碼
}catch(Exception1 e1){
    //異常處理1
}catch(Exception2 e2){
    //異常處理2
}catch(Exceptionn en){
    //異常處理n...
}
finally{
    //無論是否是否異常都會執行的地方
}

2.2.3 try、catch、finally 執行順序

執行順序通常分兩種,有異常發生執行程序、無異常執行程序。

例如當我們有示例

try{
    語句1;
    語句2
    語句n;
}catch(Exception1 e1){
    異常處理;
}
finally{
    finally語句;
}
正常語句;

try-catch-finally的執行順序

  1. 有異常發生,假設語句1發生了異常,那么程序執行順序 語句1、異常狐狸、finally語句、正常語句。
  2. 如果沒有異常發生,那么程序執行順序 語句1、語句2、語句n、finally語句、正常語句。

3 Spring Boot 中的異常處理示例

在 Spring Boot 應用程序中,通常統一處理異常的方法有
使用注解處理 @ControllerAdvice
本示例主要目的處理我們日常 Spring Boot 中的異常處理

  1. 在 Web 項目中通過 @ControllerAdvice @RestControllerAdvice 實現全局異常處理
    @ControllerAdvice@RestControllerAdvice 的區別 相當於 ControllerRestController 的區別。
  2. 在 Web 項目中實現 404、500 等狀態的頁面單獨渲染
  3. 在 Spring Boot 項目中使用 Aop 切面編程實現全局異常處理

3.1 創建時示例 Spring Boot 項目

1)File > New > Project,如下圖選擇 Spring Initializr 然后點擊 【Next】下一步

2)填寫 GroupId(包名)、Artifact(項目名) 即可。點擊 下一步
groupId=com.fishpro
artifactId=thymeleaf

3)選擇依賴 Spring Web Starter 前面打鈎。

4)項目名設置為 spring-boot-study-throwable

至此項目已經建好了,訪問 http://localhost:8084/ (注意,配置文件已經修改為了 server.port=8084),會直接拋出異常如下圖:

默認處理

如圖所示,表明 Spring Boot 具有默認的 出錯處理機制,指向了 /error 目錄。

3.2 引入依賴編輯Pom.xml

本章節用到 web 和 thymeleaf 兩個依賴,注意只有引入了 thymeleaf 后在 templates 目錄下增加 error.html 系統才能自動與 /error 路由匹配 否則會出現 Whitelabel Error Page 頁面

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

3.3 創建基於 @RestControllerAdvice 全局異常類示例

@RestControllerAdvice 注解是 Spring Boot 用於捕獲 @Controller@RestController 層系統拋出的異常(注意,如果已經編寫了 try-catch 且在 catch 模塊中沒有使用 throw 拋出異常, 則 @RestControllerAdvice 捕獲不到異常)。

@ExceptionHandler 注解用於指定方法處理的 Exception 的類型

MyRestExceptionController

如上圖所示,控制層 IndexRestController 編寫了4個 api 方法,

  • /index 是正常的方法;
  • /err 是人為拋出異常;
  • /matcherr 除數為0的異常 ;
  • /nocatch 用了 try-catch 但沒有拋出異常,不會被捕捉。
  • 四個 api 其中 /err、/matcherr 會被 MyRestExceptionController 捕捉。

本示例需要新增的文件為 2 個,分別為:

  • controller 下的 IndexRestController.java
  • exception 下的 MyRestExceptionHandler.java

本項目源碼下載

具體代碼清單如下:

3.3.1 創建用於測試的 RestController 接口類 IndexRestController


@RestController
@RequestMapping("/api")
public class IndexRestController {
    @RequestMapping("/index")
    public Map index(){
        Map<String,Object> map=new HashMap<>();
        map.put("status","0");
        map.put("msg","正常的輸出");
        return map;
    }

    /**
     * 這里人為手動拋出一個異常
     * */
    @RequestMapping("/err")
    public Map err(){

        throw new RuntimeException("拋出一個異常");
    }

    /**
     * 這里拋出的是 RuntimeException 不可查異常,雖然沒有使用 try-catch 來捕捉 但系統以及幫助我們拋出了一次
     * */
    @RequestMapping("/matcherr")
    public Map matcherr(){

        Map<String,Object> map=new HashMap<>();
        map.put("status","0");
        map.put("msg","正常的輸出");
        int j=0;
        Integer i=9/j;
        return map;
    }

    /**
     * 這里拋出的是 RuntimeException 不可查異 注意這里使用了 try-catch 來捕捉異常,但沒有拋出異常
     * */
    @RequestMapping("/nocatch")
    public Map nocatch(){

        Map<String,Object> map=new HashMap<>();
        map.put("status","0");
        map.put("msg","正常的輸出 注意這里使用了 try-catch 來捕捉異常,但沒有拋出異常,所以沒有異常,因為這里拋出的是 RuntimeException 不可查異常,系統也不會報錯。");
        int j=0;
        try{
            Integer i=9/j;
        }catch (Exception ex){

        }
        return map;
    }
}

3.3.2 創建自定義的全局異常處理類 MyRestExceptionController


/**
 * 基於@ControllerAdvice注解的全局異常統一處理只能針對於Controller層的異常
 * 為了和Controller 區分 ,我們可以指定 annotations = RestController.class,那么在Controller中拋出的異常 這里就不會被捕捉
 * */
@RestControllerAdvice(annotations = RestController.class)
public class MyRestExceptionController {
    private static final Logger logger= LoggerFactory.getLogger(MyRestExceptionController.class);

    /**
     * 處理所有的Controller層面的異常
     * */
    @ExceptionHandler(Exception.class)
    @ResponseBody
    public final Map handleAllExceptions(Exception ex, WebRequest request){
        logger.error(ex.getMessage());
        Map<String,Object> map=new HashMap<>();
        map.put("status",-1);
        map.put("msg",ex.getLocalizedMessage());

        return map;
    }
}

3.3.3 使用 Postman進行測試

http://localhost:8084/api/index

{
    "msg": "正常的輸出",
    "status": "0"
}

http://localhost:8084/api/err

{
    "msg": "拋出一個異常",
    "status": -1
}

http://localhost:8084/api/matcherr

{
    "msg": "/ by zero",
    "status": -1
}

http://localhost:8084/api/nocatch

{
    "msg": "正常的輸出 注意這里使用了 try-catch 來捕捉異常,但沒有拋出異常",
    "status": "0"
}

http://localhost:8084/api/noname 如果路由不存在,那么系統就會走404路由程序,如何統一格式,則不再此詳細闡述。

{
    "timestamp": "2019-07-12T14:53:26.513+0000",
    "status": 404,
    "error": "Not Found",
    "message": "No message available",
    "path": "/api/noname"
}

3.4 創建基於 @ControllerAdvice 全局異常類示例

@ControllerAdvice 和 @RestControllerAdvice 其實就是 @Controller 和 @RestController 的區別。直觀上就是會不會返回到前台界面的區別。

其實,無論是 @ControllerAdvice 還是 @RestControllerAdvice 都是可以捕捉 @Controller 和 @RestController 拋出的異常。

不同的是,@Controller 異常,我們往往需要更加友好的界面。下面我們使用了 thymeleaf 模板來重新定義 /error 默認路由。

MyExceptionController

如上圖所示,控制層 IndexController 編寫了2個方法,

  • error.html 在 rerouces/templates/ 目錄下,必須引入 thymeleaf 組件
  • /index 是正常的方法;
  • /index/err 是人為拋出異常,會被 MyExceptionController 捕捉;
  • /index/matcherr 除數為0的異常 會被 MyExceptionController 捕捉;

3.4.1 創建用於測試的 @Controller 文件IndexController

@Controller
public class IndexController {

    /**
     * 正常的頁面 對應 /templates/index.html 頁面
     * */
    @RequestMapping("/index")
    public String index(Model model){

        model.addAttribute("msg","這是一個index頁面的正常消息");
        return "index";
    }

    /**
     * 拋出一個 RuntimeException 異常
     * */
    @RequestMapping("/index/err")
    public String err(){

        throw new RuntimeException("拋出一個 RuntimeException 異常");
    }

    /**
     * 拋出一個 RuntimeException 異常
     * */
    @RequestMapping("/index/matherr")
    public String matherr(Model model){

        int j=0;
        int i=0;
        i=100/j;

        return "index";
    }
}

3.4.2 創建用於捕捉 @Controller 異常的全局文件MyExceptionController

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;

@ControllerAdvice(annotations = Controller.class)
public class MyExceptionController {
    private static final Logger logger= LoggerFactory.getLogger(MyExceptionController.class);
    public static final String DEFAULT_ERROR_VIEW = "error";
    /**
     * 處理所有的Controller層面的異常
     * 如果這里添加 @ResponseBody 注解 表示拋出的異常以 Rest 的方式返回,這時就系統就不會指向到錯誤頁面 /error
     * */
    @ExceptionHandler(Exception.class)
    public final ModelAndView handleAllExceptions(Exception ex, HttpServletRequest request){
        logger.error(ex.getMessage());
        ModelAndView modelAndView = new ModelAndView();

        //將異常信息設置如modelAndView
        modelAndView.addObject("msg", ex);
        modelAndView.addObject("url", request.getRequestURL());
        modelAndView.setViewName(DEFAULT_ERROR_VIEW);

        //返回ModelAndView
        return modelAndView;
    }
 
}

3.4.3 創建/error對應的出錯頁面 error.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>異常統一處理頁面</title>
</head>
<body>
this is error.html
<p th:text="${msg}"></p>
</body>
</html>

3.4.4 創建控制層對應的前端文件 index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>IndexController-index</title>
</head>
<body>
<p th:text="${msg}"></p>
</body>
</html>

3.4.5 使用瀏覽器測試

http://localhost:8084/index

這是一個index頁面的正常消息

http://localhost:8084/index/err

this is error.html
java.lang.RuntimeException: 拋出一個 RuntimeException 異常

http://localhost:8084/index/matherr

this is error.html
java.lang.ArithmeticException: / by zero

4 Spring Boot 自定義錯誤頁面

在第3章節,我們知道可以通過 建立全局異常處理類來實現 基於 @Controller 的異常統一處理。我們也可以把統一異常展示到自定義錯誤頁面。

在 Spring Boot 中使用了 ErrorController 來處理出錯請求。在 Java 8 上又提供了 BasicErrorController 他繼承與 AbstractErrorController,AbstractErrorController 又繼承於 ErrorController

4.1 基於 ErrorController 實現自定義錯誤頁面

在本章節中 需要新增3個頁面,自定義處理類、404、500、error 等頁面。其原理是根據 HttpServletResponse 的返回狀態 response.getStatus() 來判斷如果是 404 就跳轉到對應 404 路由。

基於 ErrorController 自定義錯誤頁面

  • 增加controller 下的 CustomerErrorController 頁面
  • 增加 templates/error/404.html
  • 增加 templates/error/500.html
  • 增加 templates/error/error.html
    CustomerErrorController 主要代碼
public class CustomErrorController implements ErrorController {

    private static final String ERROR_PATH = "/error";

    @RequestMapping(
            value = {ERROR_PATH},
            produces = {"text/html"}
    )

    /**
     * 用戶 Controller 帶返回的
     * */
    public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
        int code = response.getStatus();
        if (404 == code) {
            return new ModelAndView("error/404");
        } else if (403 == code) {
            return new ModelAndView("error/403");
        } else {
            return new ModelAndView("error/500");
        }

    }

    @RequestMapping(value = ERROR_PATH)
    public Map handleError(HttpServletRequest request, HttpServletResponse response) {
        Map<String,Object> map=new HashMap<>();
        int code = response.getStatus();
        if (404 == code) {
            map.put("status",404);
            map.put("msg","未找到資源文件");
        } else if (403 == code) {
            map.put("status",403);
            map.put("msg","沒有訪問權限");
        } else if (401 == code) {
            map.put("status",401);
            map.put("msg","登錄過期");
        } else {
            map.put("status",500);
            map.put("msg","服務器錯誤");
        }
        return  map;
    }

    @Override
    public String getErrorPath(){return ERROR_PATH;}
}

測試效果 瀏覽器輸入任意不存在的網站 http://localhost:8084/23/23 查看輸出

404頁面

4.2 實現自定義錯誤頁面整合到全局異常處理類中

實際上這里是可以跟上面的全局異常處理合起來的,在我們定義的 MyExceptionController
我們需要在 MyExceptionController 類中增加判定即可。

注意因為 404 異常並不是我們的異常捕捉類可以捕捉的,所以 404 頁面不在其中。

結束語

這篇文章前前后后,寫了兩天,找的參考資料很多都是不全,要么沒有交代 默認的/error 問題,要么就是沒有說明 @Controller @RestController 問題。總之我總結來有幾個問題需要解決:

  1. 如何解決默認的 /error 路由映射問題,在有 thymeleaf 與沒有的情況有什么區別
  2. 如何解決 404、505不同狀態不同映射問題
  3. @Controller @RestController是否都能攔截,有人說只能攔截 @Controller 這是不正確的, @RestController 本來就是 @Controller 演變而來,同樣是可以攔截的。
  4. @Controller 如何友好的返回
  5. @RestController 如何給遠程調用方返回錯誤信息

問題:

  1. 沒有捕捉的異常

這種情況一般是使用 try-catch 但沒有 throw 出異常導致的


關聯閱讀:

Spring Boot Log 日志使用教程

Spring Boot Thymeleaf 模板引擎的使用


免責聲明!

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



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