java的異常體系
ThrowableErrorExceptionimplimpl
Throwable
作爲最頂層的類,下面分爲Exception
(異常)和Error
(錯誤)
-
Error
程序中無法處理的錯誤,表示運行應用程序中出現了嚴重的錯誤,一半由
jvm
引起,常見的有NoClassDefFoundError
、OutOfMemoryError
-
Exception
程序運行過程中產生的異常,又分爲可查異常(checked exception) 和 不可查異常(unchecked exception)
- 可查異常(checked exception)
編譯器要求必須處理的異常,需要
try-catch
捕獲或者throws
語句拋出否則編譯不通過,常見的有ClassNotFoundException
、NoSuchMethodException
等。- 不可查異常(unchecked exception)
編譯器不會進行檢查並且不要求必須處理的異常,包括
RuntimeException
以及其子類,常見的有NullPointerException
、IllegalArgumentException
等。
全局異常處理
Spring Mvc
使用@ExceptionHandler
註解來處理由控制層拋出的異常
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ExceptionHandler {
Class<? extends Throwable>[] value() default {};
}
複製代碼
@ExceptionHandler
只有一個方法value()可以填寫的值爲繼承自Throwable
的Class
數組,也就是說一個ExceptionHandler
可以處理多個異常或錯誤
首先我們新建一個自定義異常方便後面的演示,接收一個message
參數
public class CustomException extends RuntimeException{
public CustomException(String message) {
super(message);
}
}
複製代碼
-
@RestController
和@Controller
在控制層使用如下
@RestController @Slf4j public class DomainController { @ExceptionHandler(CustomException.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public ResponseEntity<Object> domainExceptionHandler(CustomException e){ log.error("exception from DomainController", e); return ResponseEntity.status(500).body(e.getMessage()); } @GetMapping("/domain") public void test(int code){ if(code == 1){ throw new CustomException("自定義異常"); } } } 複製代碼
請求接口傳入參數code = 1,可以看到控制檯打印了
exception from DomainController 複製代碼
說明異常被
ExceptionHandler
處理了,該方式只能處理單個控制器的異常 -
@ControllerAdvice
和@RestControllerAdvice
@ControllerAdvice
和@RestControllerAdvice
可以同時處理多個的控制器,通過下列的方式可以調整處理範圍,因爲在某些情況下我們並不想讓異常控制器處理有些第三方框架的異常// 處理所有@RestController註解 @ControllerAdvice(annotations = RestController.class) public class ExampleAdvice1 {} // 處理包路徑下的所有控制器 @ControllerAdvice(basePackages = "org.example.controllers") public class ExampleAdvice2 {} // 指定特定的類處理 @ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class}) public class ExampleAdvice3 {} 複製代碼
注意:annotations和basePackages生效的前提是被處理的控制器已經被掃描成Spring Bean
在大部分情況下我們只需要配置全局的異常處理,也就是通過@RestControllerAdvice
處理所有的控制器,在單個控制器中配置的@ExceptionHandler
優先級會高於全局的,我們可以利用這點來處理有些特殊的異常或者某些定製化需求(當然最好少一些定製化需求,會導致項目後期維護困難)。
關於Error
我們現在定義一個@ExceptionHandler
處理所有的異常,如下
@ExceptionHandler({Exception.class})
public ResponseEntity<Object> handler(Exception e){
log.error("error happened ", e);
return ResponseEntity.status(500).body(e.getMessage());
}
複製代碼
修改測試代碼code = 2
時拋出Error
@GetMapping("/domain")
public void test(int code){
if(code == 1){
throw new CustomException("自定義異常");
}
if(code == 2){
throw new AssertionError("code is 2");
}
}
複製代碼
前面我們已經介紹過了Error
和Exception
的區別,在這拋出Error
,我們的異常處理應該不會處理,因爲我們只處理了所有的Exception
,並沒有處理Error
,啓動項目調用接口,控制檯得到如下信息
error happened Caused by: java.lang.AssertionError: code is 2
複製代碼
可以看到AssertionError被異常處理器處理了,這是因爲在Spring 4.3
以後,DispatcherServlet
的doDispatch
方法會處理從處理程序拋出的錯誤,使它們可用於@ExceptionHandler方法和其他場景。
...
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
...
複製代碼
統一結果返回
在實際項目中我們通常會定義統一的返回結構,常見如下
@Getter
@Builder
public class ResponseData<T> {
private long timestamp;
private int status;
private String message;
private T data;
}
複製代碼
我們不想在每個Controller上寫重複的包裝代碼,可以定義一個統一的返回處理,實現ResponseBodyAdvice
接口
@RestControllerAdvice
@Slf4j
public class ResponseHandler implements ResponseBodyAdvice {
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return false;
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
return null;
}
}
複製代碼
其中提供的兩個方法
-
supports
此方法有兩個參數
-
returnType
:控制器返回值類型 -
converterType
:http消息轉換器類型,
只有該方法返回
true
時,beforeBodyWrite
纔會執行,我們可以利用這個方法做一些配置,比如通過自定義註解@IgnoreBodyAdvice
讓某些接口不使用統一返回結構。 -
@Override
public boolean supports(final @NotNull MethodParameter methodParameter,
final Class<? extends HttpMessageConverter<?>> aClass) {
Class<?> clz = methodParameter.getDeclaringClass();
if (method == null) {
return false;
}
// 檢查註解是否存在
if (clz.isAnnotationPresent(IgnoreBodyAdvice.class)) {
return false;
} else
return !method.isAnnotationPresent(IgnoreBodyAdvice.class);
}
複製代碼
-
beforeBodyWrite
此方法有6個參數,會在
HttpMessageConverter
的write
方法之前調用-
body
:返回的消息 -
returnType
: controller的返回值類型 -
selectedContentType
:選定的消息類型,比如application/json
-
selectedConverterType
:http消息轉換器類型,比如StringHttpMessageConverter
-
request
:當前請求 -
response
:當前響應
常見的用法如下
-
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
if(body == null){
return ResponseData.builder().message("ok").build();
} else if(body instanceof ResponseData){
return body;
} else {
return ResponseData.builder().data(body).build();
}
}
複製代碼
這只是一個最簡單的例子,在實際項目可能還會有很多判斷條件,可以根據項目情況自行添加。