1. 前言
程序中出現(xiàn)異常是普遍現(xiàn)象奥洼, Java 程序員想必早已習(xí)慣巷疼,根據(jù)控制臺輸出的異常信息,分析異常產(chǎn)生的原因灵奖,然后進(jìn)行針對性處理的過程嚼沿。
Spring Boot 項目中,數(shù)據(jù)持久層桑寨、服務(wù)層到控制器層都可能拋出異常伏尼。如果我們在各層都進(jìn)行異常處理,程序代碼會顯得支離破碎尉尾,難以理解爆阶。
實(shí)際上,異成秤剑可以從內(nèi)層向外層不斷拋出辨图,最后在控制器層進(jìn)行統(tǒng)一處理。 Spring Boot 提供了全局性的異常處理機(jī)制肢藐,本節(jié)我們就分別演示下故河,默認(rèn)情況、控制器返回視圖吆豹、控制器返回 JSON 數(shù)據(jù)三種情況的異常處理方法鱼的。
2. Spring Boot 默認(rèn)異常處理機(jī)制
Spring Boot 開發(fā)的 Web 項目具備默認(rèn)的異常處理機(jī)制,無須編寫異常處理相關(guān)代碼痘煤,即可提供默認(rèn)異常機(jī)制凑阶,下面具體演示下。
2.1 使用 Spring Initializr 創(chuàng)建項目
Spring Boot 版本選擇 2.2.5 衷快,Group 為 com.imooc 宙橱, Artifact 為 spring-boot-exception-default ,生成項目后導(dǎo)入 Eclipse 開發(fā)環(huán)境。
2.2 引入項目依賴
引入 Web 項目依賴即可师郑。
實(shí)例:
<!-- web項目依賴 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
2.3 Spring Boot 默認(rèn)異常處理
我們在啟動項目环葵, Spring Boot Web 項目默認(rèn)啟動端口為 8080 ,所以直接訪問 http://127.0.0.1:8080
宝冕,顯示如下:
Spring Boot 默認(rèn)異常信息提示頁面
如上圖所示张遭,Spring Boot 默認(rèn)的異常處理機(jī)制生效,當(dāng)出現(xiàn)異常時會自動轉(zhuǎn)向 /error
路徑猬仁。
3. 控制器返回視圖時的異常處理
在使用模板引擎開發(fā) Spring Boot Web 項目時帝璧,控制器會返回視圖頁面。我們使用 Thymeleaf 演示控制器返回視圖時的異常處理方式湿刽,其他模板引擎處理方式也是相似的的烁。
3.1 使用 Spring Initializr 創(chuàng)建項目
Spring Boot 版本選擇 2.2.5 ,Group 為 com.imooc 诈闺, Artifact 為 spring-boot-exception-controller渴庆,生成項目后導(dǎo)入 Eclipse 開發(fā)環(huán)境。
3.2 引入項目依賴
引入 Web 項目依賴雅镊、熱部署依賴襟雷。此處使用 Thymeleaf 演示控制器返回視圖時的異常處理方式,所以引入 Thymeleaf 依賴仁烹。
實(shí)例:
<!-- web項目依賴 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 熱部署 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
<!-- ThymeLeaf依賴 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
3.3 定義異常類
在異常處理之前耸弄,我們應(yīng)該根據(jù)業(yè)務(wù)場景具體情況,定義一系列的異常類卓缰,習(xí)慣性的還會為各種異常分配錯誤碼计呈,如下圖為支付寶開放平臺的公共錯誤碼信息。
支付寶開放平臺錯誤碼
本節(jié)我們?yōu)榱搜菔菊骰#唵蔚亩x 2 個異常類捌显,包含錯誤碼及錯誤提示信息。
實(shí)例:
/**
* 自定義異常
*/
public class BaseException extends Exception {
/**
* 錯誤碼
*/
private int code;
/**
* 錯誤提示信息
*/
private String msg;
public BaseException(int code, String msg) {
super();
this.code = code;
this.msg = msg;
}
// 省略get set
}
實(shí)例:
/**
* 密碼錯誤異常
*/
public class PasswordException extends BaseException {
public PasswordException() {
super(10001, "密碼錯誤");
}
}
實(shí)例:
/**
* 驗(yàn)證碼錯誤異常
*/
public class VerificationCodeException extends BaseException {
public VerificationCodeException() {
super(10002, "驗(yàn)證碼錯誤");
}
}
3.4 控制器拋出異常
定義控制器 GoodsController 总寒,然后使用注解 @Controller 標(biāo)注該類扶歪,類中方法的返回值即為視圖文件名。
在 GoodsController 類定義 4 個方法摄闸,分別用于正常訪問善镰、拋出密碼錯誤異常、拋出驗(yàn)證碼錯誤異常年枕、拋出未自定義的異常媳禁,代碼如下。
實(shí)例:
/**
* 商品控制器
*/
@Controller
public class GoodsController {
/**
* 正常方法
*/
@RequestMapping("/goods")
public String goods() {
return "goods";// 跳轉(zhuǎn)到resource/templates/goods.html頁面
}
/**
* 拋出密碼錯誤異常的方法
*/
@RequestMapping("/checkPassword")
public String checkPassword() throws PasswordException {
if (true) {
throw new PasswordException();// 模擬拋出異常画切,便于測試
}
return "goods";
}
/**
* 拋出驗(yàn)證碼錯誤異常的方法
*/
@RequestMapping("/checkVerification")
public String checkVerification() throws VerificationCodeException {
if (true) {
throw new VerificationCodeException();// 模擬拋出異常,便于測試
}
return "goods";
}
/**
* 拋出未自定義的異常
*/
@RequestMapping("/other")
public String other() throws Exception {
int a = 1 / 0;// 模擬異常
return "goods";
}
}
3.5 開發(fā)基于 @ControllerAdvice 的全局異常類
@ControllerAdvice 注解標(biāo)注的類可以處理 @Controller 標(biāo)注的控制器類拋出的異常囱怕,然后進(jìn)行統(tǒng)一處理霍弹。
實(shí)例:
/**
* 控制器異常處理類
*/
@ControllerAdvice(annotations = Controller.class) // 全局異常處理
public class ControllerExceptionHandler {
@ExceptionHandler({ BaseException.class }) // 當(dāng)發(fā)生BaseException類(及其子類)的異常時毫别,進(jìn)入該方法
public ModelAndView baseExceptionHandler(BaseException e) {
ModelAndView mv = new ModelAndView();
mv.addObject("code", e.getCode());
mv.addObject("message", e.getMessage());
mv.setViewName("myerror");// 跳轉(zhuǎn)到resource/templates/myerror.html頁面
return mv;
}
@ExceptionHandler({ Exception.class }) // 當(dāng)發(fā)生Exception類的異常時,進(jìn)入該方法
public ModelAndView exceptionHandler(Exception e) {
ModelAndView mv = new ModelAndView();
mv.addObject("code", 99999);// 其他異常統(tǒng)一編碼為99999
mv.addObject("message", e.getMessage());
mv.setViewName("myerror");// 跳轉(zhuǎn)到resource/templates/myerror.html頁面
return mv;
}
}
按照 ControllerExceptionHandler 類的處理邏輯典格,當(dāng)發(fā)生 BaseException 類型的異常時岛宦,會跳轉(zhuǎn)到 myerror.html 頁面,并顯示相應(yīng)的錯誤碼和錯誤信息耍缴;當(dāng)發(fā)生其他類型的異常時砾肺,錯誤碼為 99999 ,錯誤信息為相關(guān)的異常信息防嗡。
3.6 開發(fā)前端頁面
在 resource/templates 下分別新建 goods.html 和 myerror.html 頁面变汪,作為正常訪問及發(fā)生異常時跳轉(zhuǎn)的視圖頁面。
實(shí)例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>goods.html頁面</title>
</head>
<body>
<div>商品信息頁面</div>
</body>
</html>
代碼塊12345678910
實(shí)例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>myerror.html頁面</title>
</head>
<body>
錯誤碼:
<span th:text="${code}"></span>
錯誤信息:
<span th:text="${message}"></span>
</body>
</html>
3.7 測試
啟動項目蚁趁,分別訪問控制器中的 4 個方法裙盾,結(jié)果如下:
訪問正常方法 /goods
訪問拋出自定義異常的方法 /checkPassword
et)
訪問拋出自定義異常的方法 /checkVerification
訪問拋出未自定義異常的方法 /other
可見,當(dāng)控制器方法拋出異常時他嫡,會按照全局異常類設(shè)定的邏輯統(tǒng)一處理番官。
4. 控制器返回 JSON 數(shù)據(jù)時的異常處理
在控制器類上添加 @RestController 注解,控制器方法處理完畢后會返回 JSON 格式的數(shù)據(jù)钢属。
此時徘熔,可以使用 @RestControllerAdvice 注解標(biāo)注的類 ,來捕獲 @RestController 標(biāo)注的控制器拋出的異常淆党。
4.1 使用 Spring Initializr 創(chuàng)建項目
Spring Boot 版本選擇 2.2.5 酷师,Group 為 com.imooc , Artifact 為 spring-boot-exception-restcontroller宁否,生成項目后導(dǎo)入 Eclipse 開發(fā)環(huán)境窒升。
4.2 引入項目依賴
引入 Web 項目依賴、熱部署依賴即可慕匠。
實(shí)例:
<!-- web項目依賴 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 熱部署 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
4.3 定義異常類
還是使用上文中定義的異常類即可饱须。
4.4 統(tǒng)一控制器返回數(shù)據(jù)格式
這時候,我們就需要思考一個問題了台谊。前端請求后端控制器接口后蓉媳,怎么區(qū)分后端接口是正常返回結(jié)果,還是發(fā)生了異常锅铅?
不論后端接口是正常執(zhí)行酪呻,還是中間發(fā)生了異常,最好給前端返回統(tǒng)一的數(shù)據(jù)格式盐须,便于前端統(tǒng)一分析處理玩荠。
OK,此時我們就可以封裝后端接口返回的業(yè)務(wù)邏輯對象 ResultBo ,代碼如下:
實(shí)例:
/**
* 后端接口返回的統(tǒng)一業(yè)務(wù)邏輯對象
*/
public class ResultBo<T> {
/**
* 錯誤碼 0表示沒有錯誤(異常) 其他數(shù)字代表具體錯誤碼
*/
private int code;
/**
* 后端返回消息
*/
private String msg;
/**
* 后端返回的數(shù)據(jù)
*/
private T data;
/**
* 無參數(shù)構(gòu)造函數(shù)
*/
public ResultBo() {
this.code = 0;
this.msg = "操作成功";
}
/**
* 帶數(shù)據(jù)data構(gòu)造函數(shù)
*/
public ResultBo(T data) {
this();
this.data = data;
}
/**
* 存在異常的構(gòu)造函數(shù)
*/
public ResultBo(Exception ex) {
if (ex instanceof BaseException) {
this.code = ((BaseException) ex).getCode();
this.msg = ex.getMessage();
} else {
this.code = 99999;// 其他未定義異常
this.msg = ex.getMessage();
}
}
// 省略 get set
}
4.5 控制器拋出異常
定義控制器 RestGoodsController 阶冈,并使用 @RestController 注解標(biāo)注闷尿。在其中定義 4 個方法,然后分別用于正常訪問女坑、拋出密碼錯誤異常填具、拋出驗(yàn)證碼錯誤異常式镐,以及拋出不屬于自定義異常類的異常肪凛。
實(shí)例:
/**
* Rest商品控制器
*/
@RestController
public class RestGoodsController {
/**
* 正常方法
*/
@RequestMapping("/goods")
public ResultBo goods() {
return new ResultBo<>(new ArrayList());// 正常情況下應(yīng)該返回商品列表
}
/**
* 拋出密碼錯誤異常的方法
*/
@RequestMapping("/checkPassword")
public ResultBo checkPassword() throws PasswordException {
if (true) {
throw new PasswordException();// 模擬拋出異常,便于測試
}
return new ResultBo<>(true);// 正常情況下應(yīng)該返回檢查密碼的結(jié)果true或false
}
/**
* 拋出驗(yàn)證碼錯誤異常的方法
*/
@RequestMapping("/checkVerification")
public ResultBo checkVerification() throws VerificationCodeException {
if (true) {
throw new VerificationCodeException();// 模擬拋出異常柄沮,便于測試
}
return new ResultBo<>(true);// 正常情況下應(yīng)該返回檢查驗(yàn)證碼的結(jié)果true或false
}
/**
* 拋出未自定義的異常
*/
@RequestMapping("/other")
public ResultBo other() throws Exception {
int a = 1 / 0;// 模擬異常
return new ResultBo<>(true);
}
}
4.6 開發(fā)基于 @RestControllerAdvice 的全局異常類
@RestControllerAdvice 注解標(biāo)注的類可以處理 RestController 控制器類拋出的異常碉就,然后進(jìn)行統(tǒng)一處理盟广。
實(shí)例:
/**
* Rest控制器異常處理類
*/
@RestControllerAdvice(annotations = RestController.class) // 全局異常處理
public class RestControllerExceptionHandler {
/**
* 處理BaseException類(及其子類)的異常
*/
@ExceptionHandler({ BaseException.class })
public ResultBo baseExceptionHandler(BaseException e) {
return new ResultBo(e);
}
/**
* 處理Exception類的異常
*/
@ExceptionHandler({ Exception.class })
public ResultBo exceptionHandler(Exception e) {
return new ResultBo(e);
}
}
4.7 測試
啟動項目,分別嘗試訪問控制器中的 4 個接口铝噩,結(jié)果如下衡蚂。
訪問正常方法 /goods
訪問拋出異常的方法 /checkPassword
訪問拋出異常的方法 /checkVerification
訪問拋出異常的方法 /other
5. 小結(jié)
Spring Boot 的默認(rèn)異常處理機(jī)制,實(shí)際上只能做到提醒開發(fā)者 “這個后端接口不存在” 的作用骏庸,作用非常有限毛甲。
所以我們在開發(fā) Spring Boot 項目時,需要根據(jù)項目的實(shí)際情況具被,定義各類異常玻募,并站在全局的角度統(tǒng)一處理異常。
不管項目有多少層次一姿,所有異常都可以向外拋出七咧,直到控制器層進(jìn)行集中處理。
- 對于返回視圖的控制器叮叹,如果沒發(fā)生異常就跳轉(zhuǎn)正常頁面艾栋,如果發(fā)生異常可以自定義錯誤信息頁面蛉顽。
- 對于返回 JSON 數(shù)據(jù)的控制器蝗砾,最好是定義統(tǒng)一的數(shù)據(jù)返回格式,便于前端根據(jù)返回信息進(jìn)行正承或者異常情況的處理悼粮。