天天看點

Spring Boot 進階-Spring Boot的全局異常處理機制詳解

Spring Boot 進階-Spring Boot的全局異常處理機制詳解

我們知道在軟體運作的過程中,總會出現各種各樣的問題,各種各樣的異常,而程式員的主要任務之一就是解決在程式運作過程中出現的這些異常。在很多程式員開發的代碼中我們會看到在關鍵的地方為了保證程式能夠有一個正常的回報,大量地使用了try catch finally語句。

大量的try catch語句不但增加了代碼的備援,而且還降低了代碼的可讀性。是以在Spring Boot 中就有人想将這些try catch的異常處理通過全局的方式進行處理,也就是說在我們正常的開發中就不需要再去關心這些異常的處理了。我們隻需要在一個地方進行統一的配置就可以完成全局異常的處理了。

全局異常處理介紹

其實在Spring 3.x的版本中就已經有全局異常處理的注解被提出使用了。例如@ControllerAdvice、@ExceptionHandler、@InitBinder、@ModelAttribute 等等注解。

而這些注解從字面上來看與異常有關的就隻有一個@ExceptionHandler 注解,也就是異常處理器的關鍵注解。其實在Spring中的異常處理機制有兩種,一種是全局異常處理、一種是局部異常處理。那麼什麼是全局異常處理?什麼是局部異常處理?

首先局部異常處理顧名思義就是在某些方法或者某些類上對該類中存在的異常進行處理,主要使用的注解就是@ExceptionHandler和@Controller兩個注解。這裡需要注意的是隻有指定了@Controller的類才會被@ExceptionHandler注解所捕獲到對應的異常,在實際開發過程中,Controller是不可控的,是以說,這個方式顯然不适合在大批量的Controller中使用。那麼這就需要用到我們的全局異常處理機制了

全局異常處理機制,首先來講它可以完成的異常處理在數量上遠比局部異常處理要多,其次就是它需要建立一種與局部異常處理機制不同的異常處理全局捕獲的方式。是以就出現了@ControllerAdvice注解,,這個注解搭配上@ExceptionHandler 就可以徹底解決全局異常處理的問題。當然後續還出現了@RestControllerAdvice注解,其實這個注解與@ControllerAdvice注解的差別與聯系就如同前面提到過的@Controller注解與@RestController注解是類似的,都是Response的傳回值進行了序列化的處理。

Spring Boot中的異常分類

我們知道其實異常處理是Java語言本身的特性,是以Java語言本身就會有很多的異常,更不用說是在SpringBoot中存在的異常了,也是非常多的。

這裡我們根據Spring Boot中的業務場景不同來對異常進行分類。一種就是進入業務之前的異常,一種就是在業務邏輯中的異常。如圖所示,圖檔來源網絡。

Spring Boot 進階-Spring Boot的全局異常處理機制詳解

在進入業務邏輯之前的異常一般是由Servlet操作引起的,一般需要處理的異常有如下一些

  • NoHandlerFoundException:表示用戶端的請求在服務端沒有找到對應的處理器,這個異常會抛出404的錯誤。
  • HttpRequestMethodNotSupportedException:從異常描述可以知道,它表示在使用HTTP請求的時候請求方法不支援,也就是說原本該使用GET請求的方法,使用了POST請求方法,這個異常會抛出405的錯誤
  • HttpMediaTypeNotSupportedException:這個異常表示用戶端請求的媒體類型錯誤,也就是說原本需要使用JSON資料請求的卻用的是x-form-data的資料進行請求。一般會抛出415的異常
  • MissingPathVariableException:表示路徑參數未找到,一般就是參數傳輸錯誤,引起的錯誤就是400的錯誤。

而進入業務邏輯之後的異常則是有代碼内部引起的一般常見的錯誤就是空指針異常。這個需要根據具體的業務邏輯以及對應的業務場景進行處理。

在Spring Boot中如何對這些異常進行統一處理呢?

在異常處理的時候一般開發人員關注的就是什麼地方的異常,出現異常的原因是什麼?在我們統一處理的時候需要注意哪些問題?下面我們就帶着這些問題一一來解答。

統一異常處理的步驟很簡單,我們還是以前面建立的項目來做示範

  • 第一步、需要建立一個全局異常處理的處理類
  • 第二步、在這個類上标注@RestControllerAdvice注解,或者使用@ControllerAdvice 和@ResponseBody組合來實作
  • 第三步、方法上标注上對應的異常處理的注解@ExceptionHandler,并且指定需要捕獲的異常是什麼,也可以指定多個。

根據以上的步驟我們來建立對應的處理代碼。

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    @ExceptionHandler(TestException.class)
    public String testExceptionHandler(TestException testException){
        log.info("全局異常處理",testException.getMessage());
        return "異常被全局捕獲";
    }

}           

自定義異常

public class TestException extends Exception {

    //異常資訊
    private String message;

    //構造函數
    public TestException(String message){
        super(message);
        this.message = message;
    }

    //擷取異常資訊,由于構造函數調用了super(message),不用重寫此方法
    public String getMessage(){
        return message;
    }

}           

異常抛出類

public class UserInfoException {
    private String name;
    private String password;

    public UserInfoException(String name,String password){
        this.name = name;
        this.password = password;
    }

    public void throwException(String password) throws TestException{
        if (!this.password.equals(password)){
            throw new TestException("密碼不正确!");
        }
    }
}
           

前端控制類

@RestController
public class HelloWorldController {

    @GetMapping("/hello")
    public String hello() throws TestException {
        UserInfoException ex = new UserInfoException("admin","123");
        ex.throwException("1231231");
        return "HelloWord";
    }

}           

通過這個例子,我們可以對全局的異常進行處理,在不同的地方采用同樣的方式對抛出的異常進行捕獲,并且按照自己的意願将異常結果顯示出來保證我們的程式正常運作的同時也可以讓開發人員快速的查找定位問題。

根據上面的描述在全局異常捕獲的時候可以配置多個異常使用同一個方法進行捕獲,我們修改上面的代碼如下

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    @ExceptionHandler(TestException.class)
    public String testExceptionHandler(TestException testException){
        log.info("全局異常處理",testException.getMessage());
        return "異常被全局捕獲";
    }

    @ExceptionHandler({HttpRequestMethodNotSupportedException.class,
            NoHandlerFoundException.class,
            MissingPathVariableException.class})
    public String reqeustException(Exception ex){
        log.info("測試請求類型錯誤");
        return "擷取到請求異常";
    }
}           

從代碼中我們可以看到在Handler處理方法上标注了多個注解類,那麼這些注解類注解異常捕獲的優先級又是什麼呢?

異常捕獲的優先級

對于前面提到的,有些業務異常我們是自定義的,而對于自定義的異常來講,就需要繼承Exception類。那麼如果我們這裡同時設定了父類和子類的異常捕獲,到底是哪個類優先被捕獲呢?

根據雙親委派機制可以知道,在我們類加載的時候先要在自己的加載器中找,如果找不到的情況下才會向父類或者父類的父類去找。那麼異常比對也是一樣,首先會去比對到自己的異常,如果沒有進行處理才會去比對父類的異常。也就是說在這裡的異常捕獲優先級是精确比對的,就是說你設定了哪個哪個就會先被比對。

可以通過檢視org.springframework.web.method.annotation.ExceptionHandlerMethodResolver類的源碼發現

/**
	 * Return the {@link Method} mapped to the given exception type, or
	 * {@link #NO_MATCHING_EXCEPTION_HANDLER_METHOD} if none.
	 */
	private Method getMappedMethod(Class<? extends Throwable> exceptionType) {
		List<Class<? extends Throwable>> matches = new ArrayList<>();
    // 周遊所有異常處理的類
		for (Class<? extends Throwable> mappedException : this.mappedMethods.keySet()) {
			// 判斷是否抛出異常的父類,如果是添加到集合中
      if (mappedException.isAssignableFrom(exceptionType)) {
				matches.add(mappedException);
			}
		}
    // 如果沒有比對則按照規則進行排序
		if (!matches.isEmpty()) {
			if (matches.size() > 1) {
				matches.sort(new ExceptionDepthComparator(exceptionType));
			}
      // 擷取第一個進行抛出
			return this.mappedMethods.get(matches.get(0));
		}
		else {
			return NO_MATCHING_EXCEPTION_HANDLER_METHOD;
		}
	}           

檢視源碼我們會發現其中有一步操作是對規則排序那麼我們就來看看排序的規則是什麼代碼如下。會看到源碼中一直遞歸找到其繼承的父類。也就是說排序完成之後第一個擷取到的規則就是它自己本身。

private int getDepth(Class<?> declaredException, Class<?> exceptionToMatch, int depth) {
		if (exceptionToMatch.equals(declaredException)) {
			// Found it!
			return depth;
		}
		// If we've gone as far as we can go and haven't found it...
		if (exceptionToMatch == Throwable.class) {
			return Integer.MAX_VALUE;
		}
		return getDepth(declaredException, exceptionToMatch.getSuperclass(), depth + 1);
	}           

總結

全局異常處理,在Spring Boot的開發中是經常會被用到,并且也極大的提升了開發效率。希望這篇文章能給大家帶來幫助。對于源碼級的分析,在後續深入了解Spring Boot原理的時候我們再去進行分析,這裡隻是淺析了一點源碼,喜歡的讀者可以關注筆者,後續還有很多幹貨帶給大家。

繼續閱讀