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

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

