Spring Boot 中的全局異常處理
本來已收錄到我寫的10萬字Springboot經(jīng)典學(xué)習(xí)筆記中,筆記在持續(xù)更新……文末有領(lǐng)取方式
在項目開發(fā)過程中,不管是對底層數(shù)據(jù)庫的操作過程,還是業(yè)務(wù)層的處理過程,還是控制層的處理過程,都不可避免會遇到各種可預(yù)知的、不可預(yù)知的異常需要處理。如果對每個過程都單獨作異常處理,那系統(tǒng)的代碼耦合度會變得很高,此外,開發(fā)工作量也會加大而且不好統(tǒng)一,這也增加了代碼的維護成本。
針對這種實際情況,我們需要將所有類型的異常處理從各處理過程解耦出來,這樣既保證了相關(guān)處理過程的功能單一,也實現(xiàn)了異常信息的統(tǒng)一處理和維護。同時,我們也不希望直接把異常拋給用戶,應(yīng)該對異常進行處理,對錯誤信息進行封裝,然后返回一個友好的信息給用戶。這節(jié)主要總結(jié)一下項目中如何使用 Spring Boot 如何攔截并處理全局的異常。
1. 定義返回的統(tǒng)一 json 結(jié)構(gòu)
前端或者其他服務(wù)請求本服務(wù)的接口時,該接口需要返回對應(yīng)的 json 數(shù)據(jù),一般該服務(wù)只需要返回請求著需要的參數(shù)即可,但是在實際項目中,我們需要封裝更多的信息,比如狀態(tài)碼 code、相關(guān)信息 msg 等等,這一方面是在項目中可以有個統(tǒng)一的返回結(jié)構(gòu),整個項目組都適用,另一方面是方便結(jié)合全局異常處理信息,因為異常處理信息中一般我們需要把狀態(tài)碼和異常內(nèi)容反饋給調(diào)用方。
這個統(tǒng)一的 json 結(jié)構(gòu)這可以參考第02課:Spring Boot 返回 JSON 數(shù)據(jù)及數(shù)據(jù)封裝中封裝的統(tǒng)一 json 結(jié)構(gòu),本節(jié)內(nèi)容我們簡化一下,只保留狀態(tài)碼 code 和異常信息 msg即可。如下:
public?class?JsonResult?{
????/**
?????*?異常碼
?????*/
????protected?String?code;
????/**
?????*?異常信息
?????*/
????protected?String?msg;
?
????public?JsonResult()?{
????????this.code?=?"200";
????????this.msg?=?"操作成功";
????}
????
????public?JsonResult(String?code,?String?msg)?{
????????this.code?=?code;
????????this.msg?=?msg;
????}
?//?get?set
}
2. 處理系統(tǒng)異常
新建一個 GlobalExceptionHandler 全局異常處理類,然后加上?@ControllerAdvice?注解即可攔截項目中拋出的異常,如下:
@ControllerAdvice
@ResponseBody
public?class?GlobalExceptionHandler?{
?//?打印log
????private?static?final?Logger?logger?=?LoggerFactory.getLogger(GlobalExceptionHandler.class);
????//?……
}
我們點開?@ControllerAdvice?注解可以看到,@ControllerAdvice?注解包含了?@Component?注解,說明在 Spring Boot 啟動時,也會把該類作為組件交給 Spring 來管理。除此之外,該注解還有個?basePackages?屬性,該屬性是用來攔截哪個包中的異常信息,一般我們不指定這個屬性,我們攔截項目工程中的所有異常。@ResponseBody?注解是為了異常處理完之后給調(diào)用方輸出一個 json 格式的封裝數(shù)據(jù)。
在項目中如何使用呢?Spring Boot 中很簡單,在方法上通過?@ExceptionHandler?注解來指定具體的異常,然后在方法中處理該異常信息,最后將結(jié)果通過統(tǒng)一的 json 結(jié)構(gòu)體返回給調(diào)用者。下面我們舉幾個例子來說明如何來使用。
2.1 處理參數(shù)缺失異常
在前后端分離的架構(gòu)中,前端請求后臺的接口都是通過 rest 風(fēng)格來調(diào)用,有時候,比如 POST 請求 需要攜帶一些參數(shù),但是往往有時候參數(shù)會漏掉。另外,在微服務(wù)架構(gòu)中,涉及到多個微服務(wù)之間的接口調(diào)用時,也可能出現(xiàn)這種情況,此時我們需要定義一個處理參數(shù)缺失異常的方法,來給前端或者調(diào)用方提示一個友好信息。
參數(shù)缺失的時候,會拋出?HttpMessageNotReadableException,我們可以攔截該異常,做一個友好處理,如下:
/**
*?缺少請求參數(shù)異常
*?@param?ex?HttpMessageNotReadableException
*?@return
*/
@ExceptionHandler(MissingServletRequestParameterException.class)
@ResponseStatus(value?=?HttpStatus.BAD_REQUEST)
public?JsonResult?handleHttpMessageNotReadableException(
????MissingServletRequestParameterException?ex)?{
????logger.error("缺少請求參數(shù),{}",?ex.getMessage());
????return?new?JsonResult("400",?"缺少必要的請求參數(shù)");
}
我們來寫個簡單的 Controller 測試一下該異常,通過 POST 請求方式接收兩個參數(shù):姓名和密碼。
@RestController
@RequestMapping("/exception")
public?class?ExceptionController?{
????private?static?final?Logger?logger?=?LoggerFactory.getLogger(ExceptionController.class);
????@PostMapping("/test")
????public?JsonResult?test(@RequestParam("name")?String?name,
???????????????????????????@RequestParam("pass")?String?pass)?{
????????logger.info("name:{}",?name);
????????logger.info("pass:{}",?pass);
????????return?new?JsonResult();
????}
}
然后使用 Postman 來調(diào)用一下該接口,調(diào)用的時候,只傳姓名,不傳密碼,就會拋缺少參數(shù)異常,該異常被捕獲之后,就會進入我們寫好的邏輯,給調(diào)用方返回一個友好信息,如下:

2.2 處理空指針異常
空指針異常是開發(fā)中司空見慣的東西了,一般發(fā)生的地方有哪些呢?
先來聊一聊一些注意的地方,比如在微服務(wù)中,經(jīng)常會調(diào)用其他服務(wù)獲取數(shù)據(jù),這個數(shù)據(jù)主要是 json 格式的,但是在解析 json 的過程中,可能會有空出現(xiàn),所以我們在獲取某個 jsonObject 時,再通過該 jsonObject 去獲取相關(guān)信息時,應(yīng)該要先做非空判斷。
還有一個很常見的地方就是從數(shù)據(jù)庫中查詢的數(shù)據(jù),不管是查詢一條記錄封裝在某個對象中,還是查詢多條記錄封裝在一個 List 中,我們接下來都要去處理數(shù)據(jù),那么就有可能出現(xiàn)空指針異常,因為誰也不能保證從數(shù)據(jù)庫中查出來的東西就一定不為空,所以在使用數(shù)據(jù)時一定要先做非空判斷。
對空指針異常的處理很簡單,和上面的邏輯一樣,將異常信息換掉即可。如下:
@ControllerAdvice
@ResponseBody
public?class?GlobalExceptionHandler?{
????private?static?final?Logger?logger?=?LoggerFactory.getLogger(GlobalExceptionHandler.class);
????/**
?????*?空指針異常
?????*?@param?ex?NullPointerException
?????*?@return
?????*/
????@ExceptionHandler(NullPointerException.class)
????@ResponseStatus(value?=?HttpStatus.INTERNAL_SERVER_ERROR)
????public?JsonResult?handleTypeMismatchException(NullPointerException?ex)?{
????????logger.error("空指針異常,{}",?ex.getMessage());
????????return?new?JsonResult("500",?"空指針異常了");
????}
}
這個我就不測試了,代碼中 ExceptionController 有個?testNullPointException?方法,模擬了一個空指針異常,我們在瀏覽器中請求一下對應(yīng)的 url 即可看到返回的信息:
{"code":"500","msg":"空指針異常了"}
2.3 一勞永逸?
當(dāng)然了,異常很多,比如還有 RuntimeException,數(shù)據(jù)庫還有一些查詢或者操作異常等等。由于 Exception 異常是父類,所有異常都會繼承該異常,所以我們可以直接攔截 Exception 異常,一勞永逸:
@ControllerAdvice
@ResponseBody
public?class?GlobalExceptionHandler?{
????private?static?final?Logger?logger?=?LoggerFactory.getLogger(GlobalExceptionHandler.class);
????/**
?????*?系統(tǒng)異常?預(yù)期以外異常
?????*?@param?ex
?????*?@return
?????*/
????@ExceptionHandler(Exception.class)
????@ResponseStatus(value?=?HttpStatus.INTERNAL_SERVER_ERROR)
????public?JsonResult?handleUnexpectedServer(Exception?ex)?{
????????logger.error("系統(tǒng)異常:",?ex);
????????return?new?JsonResult("500",?"系統(tǒng)發(fā)生異常,請聯(lián)系管理員");
????}
}
但是項目中,我們一般都會比較詳細(xì)的去攔截一些常見異常,攔截 Exception 雖然可以一勞永逸,但是不利于我們?nèi)ヅ挪榛蛘叨ㄎ粏栴}。實際項目中,可以把攔截 Exception 異常寫在 GlobalExceptionHandler 最下面,如果都沒有找到,最后再攔截一下 Exception 異常,保證輸出信息友好。
3. 攔截自定義異常
在實際項目中,除了攔截一些系統(tǒng)異常外,在某些業(yè)務(wù)上,我們需要自定義一些業(yè)務(wù)異常,比如在微服務(wù)中,服務(wù)之間的相互調(diào)用很平凡,很常見。要處理一個服務(wù)的調(diào)用時,那么可能會調(diào)用失敗或者調(diào)用超時等等,此時我們需要自定義一個異常,當(dāng)調(diào)用失敗時拋出該異常,給 GlobalExceptionHandler 去捕獲。
3.1 定義異常信息
由于在業(yè)務(wù)中,有很多異常,針對不同的業(yè)務(wù),可能給出的提示信息不同,所以為了方便項目異常信息管理,我們一般會定義一個異常信息枚舉類。比如:
/**
?*?業(yè)務(wù)異常提示信息枚舉類
?*?@author?shengwu?ni
?*/
public?enum?BusinessMsgEnum?{
????/**?參數(shù)異常?*/
????PARMETER_EXCEPTION("102",?"參數(shù)異常!"),
????/**?等待超時?*/
????SERVICE_TIME_OUT("103",?"服務(wù)調(diào)用超時!"),
????/**?參數(shù)過大?*/
????PARMETER_BIG_EXCEPTION("102",?"輸入的圖片數(shù)量不能超過50張!"),
????/**?500?:?一勞永逸的提示也可以在這定義?*/
????UNEXPECTED_EXCEPTION("500",?"系統(tǒng)發(fā)生異常,請聯(lián)系管理員!");
????//?還可以定義更多的業(yè)務(wù)異常
????/**
?????*?消息碼
?????*/
????private?String?code;
????/**
?????*?消息內(nèi)容
?????*/
????private?String?msg;
????private?BusinessMsgEnum(String?code,?String?msg)?{
????????this.code?=?code;
????????this.msg?=?msg;
????}
?//?set?get方法
}
3.2 攔截自定義異常
然后我們可以定義一個業(yè)務(wù)異常,當(dāng)出現(xiàn)業(yè)務(wù)異常時,我們就拋這個自定義的業(yè)務(wù)異常即可。比如我們定義一個 BusinessErrorException 異常,如下:
/**
?*?自定義業(yè)務(wù)異常
?*?@author?shengwu?ni
?*/
public?class?BusinessErrorException?extends?RuntimeException?{
????
????private?static?final?long?serialVersionUID?=?-7480022450501760611L;
????/**
?????*?異常碼
?????*/
????private?String?code;
????/**
?????*?異常提示信息
?????*/
????private?String?message;
????public?BusinessErrorException(BusinessMsgEnum?businessMsgEnum)?{
????????this.code?=?businessMsgEnum.code();
????????this.message?=?businessMsgEnum.msg();
????}
?//?get?set方法
}
在構(gòu)造方法中,傳入我們上面自定義的異常枚舉類,所以在項目中,如果有新的異常信息需要添加,我們直接在枚舉類中添加即可,很方便,做到統(tǒng)一維護,然后再攔截該異常時獲取即可。
@ControllerAdvice
@ResponseBody
public?class?GlobalExceptionHandler?{
????private?static?final?Logger?logger?=?LoggerFactory.getLogger(GlobalExceptionHandler.class);
????/**
?????*?攔截業(yè)務(wù)異常,返回業(yè)務(wù)異常信息
?????*?@param?ex
?????*?@return
?????*/
????@ExceptionHandler(BusinessErrorException.class)
????@ResponseStatus(value?=?HttpStatus.INTERNAL_SERVER_ERROR)
????public?JsonResult?handleBusinessError(BusinessErrorException?ex)?{
????????String?code?=?ex.getCode();
????????String?message?=?ex.getMessage();
????????return?new?JsonResult(code,?message);
????}
}
在業(yè)務(wù)代碼中,我們可以直接模擬一下拋出業(yè)務(wù)異常,測試一下:
@RestController
@RequestMapping("/exception")
public?class?ExceptionController?{
????private?static?final?Logger?logger?=?LoggerFactory.getLogger(ExceptionController.class);
????@GetMapping("/business")
????public?JsonResult?testException()?{
????????try?{
????????????int?i?=?1?/?0;
????????}?catch?(Exception?e)?{
????????????throw?new?BusinessErrorException(BusinessMsgEnum.UNEXPECTED_EXCEPTION);
????????}
????????return?new?JsonResult();
????}
}
運行一下項目,測試一下,返回 json 如下,說明我們自定義的業(yè)務(wù)異常捕獲成功:
{"code":"500","msg":"系統(tǒng)發(fā)生異常,請聯(lián)系管理員!"}
4. 總結(jié)
本節(jié)課程主要講解了Spring Boot 的全局異常處理,包括異常信息的封裝、異常信息的捕獲和處理,以及在實際項目中,我們用到的自定義異常枚舉類和業(yè)務(wù)異常的捕獲與處理,在項目中運用的非常廣泛,基本上每個項目中都需要做全局異常處理。
該文已收錄到我寫的《10萬字Springboot經(jīng)典學(xué)習(xí)筆記》中,點擊下面小卡片,進入【Java開發(fā)寶典】,回復(fù):筆記,即可免費獲取。
點贊是最大的支持?

