說到異常處理,我們都知道使用 try-catch
可以捕捉異常,可以 throws
拋出異常。那么在 Spring Boot 中我們如何處理異常,如何是的處理更加優雅,如何全局處理異常。是本章討論解決的問題。
首先讓我們簡單了解或重新學習下 Java 的異常機制。
1 Java 異常機制概述
Spring Boot 的所有異常處理都基於 java 的。
1.1 Java 異常類圖
- Java 內部的異常類
Throwable
包括了Exception
和Error
兩大類,所有的異常類都是Object
對象。 Error
是不可捕捉的異常,通俗的理解就是由於 java 內部 jvm 引起的不可預見的異常,比如 java 虛擬機運行錯誤,當內存資源錯誤,將會出現OutOfMemoryError
。此時 java 虛擬機會選擇終止線程。Excetpion
異常是程序本身引起的,它又分為運行時異常RuntimeException
,和非運行時(編譯時)IOException
等異常。- 運行時異常
RuntimeException
例如:除數為零,將引發ArrayIndexOutOfBoundException
異常。 - 非運行異常都是可查可捕捉的。Java 編譯器會告訴程序他錯了,錯在哪里,正確的建議什么。我們可以通過 throws 配合
try-catch
來處理。
1.2 Exception 運行時異常和編譯異常
- 運行時異常 即
RuntimeException
類型下的異常 - 編譯異常 即
Exception
類型下除了RuntimeException
類型的異常,例如IOException
1.3 可查異常與不可查異常
- 可查異常 即
Exception
類型下除了RuntimeException
類型的異常,都是可查的,具有可查性。 - 不可查異常 錯誤類
Error
和RuntimeException
類型的異常都是不可查的,具有不可查性。
2 Java 異常處理機制
2.1 異常處理機制的分類
在 Java 應用程序中,異常處理機制為:拋出異常,捕捉異常。
-
拋出異常:當一個方法出現錯誤引發異常時,方法創建異常對象並交付運行時系統,異常對象中包含了異常類型和異常出現時的程序狀態等異常信息。運行時系統負責尋找處置異常的代碼並執行。
-
捕獲異常:在方法拋出異常之后,運行時系統將轉為尋找合適的異常處理器(exception handler)。潛在的異常處理器是異常發生時依次存留在調用棧中的方法的集合。當異常處理器所能處理的異常類型與方法拋出的異常類型相符時,即為合適 的異常處理器。運行時系統從發生異常的方法開始,依次回查調用棧中的方法,直至找到含有合適異常處理器的方法並執行。當運行時系統遍歷調用棧而未找到合適 的異常處理器,則運行時系統終止。同時,意味着Java程序的終止。
針對不同的異常類型,Java 對處理的要求不一樣
- Error 錯誤,由於不可捕捉,不可查詢,Java 允許不做任何處理。
- 對於運行時異常 RuntimeException 不可查詢異常,Java 允許程序忽略運行時異常,Java 系統會自動記錄並處理。
- 對於所有可查異常都可捕捉,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 流程規則
try
、catch
語句,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
try
、catch
、finally
語句中,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語句;
}
正常語句;
- 有異常發生,假設語句1發生了異常,那么程序執行順序 語句1、異常狐狸、finally語句、正常語句。
- 如果沒有異常發生,那么程序執行順序 語句1、語句2、語句n、finally語句、正常語句。
3 Spring Boot 中的異常處理示例
在 Spring Boot 應用程序中,通常統一處理異常的方法有
使用注解處理 @ControllerAdvice
本示例主要目的處理我們日常 Spring Boot 中的異常處理
- 在 Web 項目中通過
@ControllerAdvice
@RestControllerAdvice
實現全局異常處理
@ControllerAdvice
和@RestControllerAdvice
的區別 相當於Controller
和RestController
的區別。 - 在 Web 項目中實現 404、500 等狀態的頁面單獨渲染
- 在 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 的類型
如上圖所示,控制層 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"
}
{
"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 默認路由。
如上圖所示,控制層 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 使用瀏覽器測試
這是一個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 路由。
- 增加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 問題。總之我總結來有幾個問題需要解決:
- 如何解決默認的 /error 路由映射問題,在有 thymeleaf 與沒有的情況有什么區別
- 如何解決 404、505不同狀態不同映射問題
- @Controller @RestController是否都能攔截,有人說只能攔截 @Controller 這是不正確的, @RestController 本來就是 @Controller 演變而來,同樣是可以攔截的。
- @Controller 如何友好的返回
- @RestController 如何給遠程調用方返回錯誤信息
問題:
- 沒有捕捉的異常
這種情況一般是使用 try-catch 但沒有 throw 出異常導致的
關聯閱讀: