Spring Mvc全局異常處理及統一結果返回 java的異常體系 全局異常處理 關於Error 統一結果返回

java的異常體系

ThrowableErrorExceptionimplimpl

Throwable作爲最頂層的類,下面分爲Exception(異常)和Error(錯誤)

  • Error

    程序中無法處理的錯誤,表示運行應用程序中出現了嚴重的錯誤,一半由jvm引起,常見的有NoClassDefFoundErrorOutOfMemoryError

  • Exception

    程序運行過程中產生的異常,又分爲可查異常(checked exception)不可查異常(unchecked exception)

    • 可查異常(checked exception)

    編譯器要求必須處理的異常,需要try-catch捕獲或者throws語句拋出否則編譯不通過,常見的有ClassNotFoundExceptionNoSuchMethodException等。

    • 不可查異常(unchecked exception)

    編譯器不會進行檢查並且不要求必須處理的異常,包括RuntimeException以及其子類,常見的有NullPointerExceptionIllegalArgumentException等。

全局異常處理

Spring Mvc使用@ExceptionHandler註解來處理由控制層拋出的異常

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ExceptionHandler {
    Class<? extends Throwable>[] value() default {};
}
複製代碼

@ExceptionHandler只有一個方法value()可以填寫的值爲繼承自ThrowableClass數組,也就是說一個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");
        }
    }
複製代碼

前面我們已經介紹過了ErrorException的區別,在這拋出Error,我們的異常處理應該不會處理,因爲我們只處理了所有的Exception,並沒有處理Error,啓動項目調用接口,控制檯得到如下信息

error happened Caused by: java.lang.AssertionError: code is 2
複製代碼

可以看到AssertionError被異常處理器處理了,這是因爲在Spring 4.3以後,DispatcherServletdoDispatch方法會處理從處理程序拋出的錯誤,使它們可用於@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個參數,會在HttpMessageConverterwrite方法之前調用

    • 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();
    }
}
複製代碼

這只是一個最簡單的例子,在實際項目可能還會有很多判斷條件,可以根據項目情況自行添加。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章