八、Spring Boot 中的全局異常處理

前言:上一節對 SpringBoot 的Spring Boot 集成 Thymeleaf 模板引擎做了一個介紹,本節主要對Spring Boot 中的全局異常處理講解和分析。

在項目開發過程中,不管是對底層數據庫的操作過程,還是業務層的處理過程,還是控制層的處理過程,都不可避免會遇到各種可預知的、不可預知的異常需要處理。如果對 每個過程都單獨作異常處理,那系統的代碼耦合度會變得很高,此外,開發工作量也會 加大而且不好統一,這也增加了代碼的維護成本。

針對這種實際情況,我們需要將所有類型的異常處理從各處理過程解耦出來,這樣既保 證了相關處理過程的功能單一,也實現了異常信息的統一處理和維護。同時,我們也不 希望直接把異常拋給用戶,應該對異常進行處理,對錯誤信息進行封裝,然後返回一個 友好的信息給用戶。這節主要總結一下項目中如何使用 Spring Boot 如何攔截並處理全 局的異常。

1. 定義返回的統一 json 結構

前端或者其他服務請求本服務的接口時,該接口需要返回對應的 json 數據,一般該服務只需要返回請求着需要的參數即可,但是在實際項目中,我們需要封裝更多的信息, 比如狀態碼 code、相關信息 msg 等等,這一方面是在項目中可以有個統一的返回結構,整個項目組都適用,另一方面是方便結合全局異常處理信息,因爲異常處理信息中 一般我們需要把狀態碼和異常內容反饋給調用方。

這個統一的 json 結構這可以參考Spring Boot 返回 JSON 數據及數據封裝中 封裝的統一 json 結構,本節內容我們簡化一下,只保留狀態碼 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. 處理系統異常

新建一個 GlobalExceptionHandler 全局異常處理類,然後加上 @ControllerAdvice 註解即可攔截項目中拋出的異常,如下

@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {

	private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
	//......
}

我們點開 @ControllerAdvice 註解可以看到,@ControllerAdvice 註解包含了 @Component 註解,說明在 Spring Boot 啓動時,也會把該類作爲組件交給 Spring 來管 理。除此之外,該註解還有個 basePackages 屬性,該屬性是用來攔截哪個包中的異常信息,一般我們不指定這個屬性,我們攔截項目工程中的所有異常。 @ResponseBody 註解是爲了異常處理完之後給調用方輸出一個 json 格式的封裝數據。 在項目中如何使用呢?Spring Boot 中很簡單,在方法上通過 @ExceptionHandler 注 解來指定具體的異常,然後在方法中處理該異常信息,最後將結果通過統一的 json 結構體返回給調用者。下面我們舉幾個例子來說明如何來使用。

2.1 處理參數缺失異常

在前後端分離的架構中,前端請求後臺的接口都是通過 rest 風格來調用,有時候,比如 POST 請求 需要攜帶一些參數,但是往往有時候參數會漏掉。另外,在微服務架構中,涉及到多個微服務之間的接口調用時,也可能出現這種情況,此時我們需要定義一 個處理參數缺失異常的方法,來給前端或者調用方提示一個友好信息。

參數缺失的時候,會拋出 HttpMessageNotReadableException,我們可以攔截該異 常,做一個友好處理,如下:

	/**
	 * 缺少請求參數異常
	 * @param ex HttpMessageNotReadableException
	 * @return
	 */
	@ExceptionHandler(MissingServletRequestParameterException.class)
	@ResponseStatus(value = HttpStatus.BAD_REQUEST)
	public JsonResult handleHttpMessageNotReadableException(
			MissingServletRequestParameterException ex) {
		logger.error("缺少請求參數,{}", ex.getMessage());
		return new JsonResult("400", "缺少必要的請求參數");
	}

我們來寫個簡單的 Controller 測試一下該異常,通過 POST 請求方式接收兩個參數: 姓名和密碼。

@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 來調用一下該接口,調用的時候,只傳姓名,不傳密碼,就會拋缺 少參數異常,該異常被捕獲之後,就會進入我們寫好的邏輯,給調用方返回一個友好信 息,如下:

2.2 處理空指針異常

空指針異常是開發中司空見慣的東西了,一般發生的地方有哪些呢? 先來聊一聊一些注意的地方,比如在微服務中,經常會調用其他服務獲取數據,這個數據主要是 json 格式的,但是在解析 json 的過程中,可能會有空出現,所以我們在獲取 某個 jsonObject 時,再通過該 jsonObject 去獲取相關信息時,應該要先做非空判斷。 還有一個很常見的地方就是從數據庫中查詢的數據,不管是查詢一條記錄封裝在某個對 象中,還是查詢多條記錄封裝在一個 List 中,我們接下來都要去處理數據,那麼就有可能出現空指針異常,因爲誰也不能保證從數據庫中查出來的東西就一定不爲空,所以在 使用數據時一定要先做非空判斷。 對空指針異常的處理很簡單,和上面的邏輯一樣,將異常信息換掉即可。如下:

/**
	 * 空指針異常
	 * @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 方法,模擬了一個空指針異常,我們在瀏覽器中請求一下對應的 url 即可看到返回的信息:

{"code":"500","msg":"空指針異常了"}

2.3 一勞永逸?

當然了,異常很多,比如還有 RuntimeException,數據庫還有一些查詢或者操作異常 等等。由於 Exception 異常是父類,所有異常都會繼承該異常,所以我們可以直接攔截 Exception 異常,一勞永逸:

/**
	 * 系統異常 預期以外異常
	 * @param ex
	 * @return
	 */
	@ExceptionHandler(Exception.class)
	@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
	public JsonResult handleUnexpectedServer(Exception ex) {
		logger.error("系統異常:", ex);
		return new JsonResult("500", "系統發生異常,請聯繫管理員");
	}

但是項目中,我們一般都會比較詳細的去攔截一些常見異常,攔截 Exception 雖然可以 一勞永逸,但是不利於我們去排查或者定位問題。實際項目中,可以把攔截 Exception 異常寫在 GlobalExceptionHandler 最下面,如果都沒有找到,最後再攔截一下 Exception 異常,保證輸出信息友好。

3. 攔截自定義異常

在實際項目中,除了攔截一些系統異常外,在某些業務上,我們需要自定義一些業務異 常,比如在微服務中,服務之間的相互調用很平凡,很常見。要處理一個服務的調用 時,那麼可能會調用失敗或者調用超時等等,此時我們需要自定義一個異常,當調用失 敗時拋出該異常,給 GlobalExceptionHandler 去捕獲。

3.1 定義異常信息

由於在業務中,有很多異常,針對不同的業務,可能給出的提示信息不同,所以爲了方 便項目異常信息管理,我們一般會定義一個異常信息枚舉類。比如:

/**
 * 業務異常提示信息枚舉類
 * @author shengwu ni
 */
public enum BusinessMsgEnum {
	/**
	 * 參數異常
	 */
	PARMETER_EXCEPTION("102", "參數異常!"),
	/**
	 * 等待超時
	 */
	SERVICE_TIME_OUT("103", "服務調用超時!"),
	/**
	 * 參數過大
	 */
	PARMETER_BIG_EXCEPTION("102", "輸入的圖片數量不能超過50張!"),
	/**
	 * 500 : 發生異常
	 */
	UNEXPECTED_EXCEPTION("500", "系統發生異常,請聯繫管理員!");

	/**
	 * 消息碼
	 */
	private String code;
	/**
	 * 消息內容
	 */
	private String msg;

	private BusinessMsgEnum(String code, String msg) {
		this.code = code;
		this.msg = msg;
	}
	// set get 方法
}

3.2 攔截自定義異常

然後我們可以定義一個業務異常,當出現業務異常時,我們就拋這個自定義的業務異常即可。比如我們定義一個 BusinessErrorException 異常,如下:

/**
 * 自定義業務異常
 * @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 方法
}

在構造方法中,傳入我們上面自定義的異常枚舉類,所以在項目中,如果有新的異常信 息需要添加,我們直接在枚舉類中添加即可,很方便,做到統一維護,然後再攔截該異常時獲取即可。

@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {

	private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);

	/**
	 * 攔截業務異常,返回業務異常信息
	 *
	 * @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);
	}

}

在業務代碼中,我們可以直接模擬一下拋出業務異常,測試一下:

@GetMapping("/business")
public JsonResult testException() {
	try {
		int i = 1 / 0;
	} catch (Exception e) {
		throw new BusinessErrorException(BusinessMsgEnum.UNEXPECTED_EXCEPTION);
	}
	return new JsonResult();
}
運行一下項目,測試一下,返回 json 如下,說明我們自定義的業務異常捕獲成功:
{"code":"500","msg":"系統發生異常,請聯繫管理員!"}

4. 總結

本節課程主要講解了 Spring Boot 的全局異常處理,包括異常信息的封裝、異常信息的捕獲和處理,以及在實際項目中,我們用到的自定義異常枚舉類和業務異常的捕獲與處理,在項目中運用的非常廣泛,基本上每個項目中都需要做全局異常處理。


最後,希望能和大家開啓一段充實的學習歷程,願大家都能突破職場瓶頸,提升競爭力。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章