(六講)Spring Boot REST API異常處理指南

可以正確的處理REST API程序拋出的異常以及返回友好的異常信息是一件很是重要的事情,由於它能夠幫助API客戶端正確的對服務端的問題做出正確的響應。這有助於提升REST API的服務質量。Spring Boot默認返回的異常信息對於API客戶端來講是晦澀難懂的,只有開發者纔會關注那些堆棧異常報告。在本講中,將對如何處理好Spring REST API異常信息作一個梳理。

最近一段時間,Spring Boot成爲了Java開發圈子的網紅,愈來愈多的開發者選擇Spring Boot來構建REST API。使用Spring Boot,可以幫助開發者減小模板代碼和配置文件的編寫工做量。Spring Boot開箱即用的特性,受到廣大開發者的熱寵。在本講中,經過一個精簡的Demo項目,着重介紹一些Spring Boot REST API的異常處理技巧。html

若是你不想閱讀本次內容,只是想快速得到相關的源碼,那你能夠直接跳轉到文章的結尾,找到Gihub倉庫連接,經過該連接,你能夠輕鬆的得到本次內容的所有源碼。

1. 定義明確的異常信息

當程序發送錯誤時,不該該將晦澀的堆棧報告信息返回給API客戶端,從某種意義將,這是一種不禮貌的和不負責任的行爲。如今,咱們將模擬這樣一個需求,API客戶端能夠向服務端發送請求以獲取一個或者多個用戶信息,同時還能夠發送請求建立一個新的用戶信息。下面是大體的一個API信息:java

API 名稱 說明
GET /users/{userId} 根據用戶ID檢索用戶信息,若是沒有找到,則返回用戶未找到異常信息
GET /users 根據傳入的ID集合,檢索用戶信息,若未找到,返回未找到用戶異常信息
POST /users 建立一個新的用戶

Spring MVC爲咱們提供了一些頗有用的功能,以幫助咱們解決系統的異常信息,並將有用的提示信息返回給API客戶端。git

以 POST /users 建立一個新用戶爲例,當咱們提供正常的用戶數據並請求此接口時,REST API將返回以下的提示信息:github

{
  "id": 2,
  "username": "wukong",
  "age": 52,
  "height": 170
}

如今,將用戶年齡修改成200歲,身高修改成500釐米,用戶名爲rulai ,在此請求此REST API,觀察API的返回信息:spring

{
  "restapierror": {
    "status": "BAD_REQUEST",
    "timestamp": "2019-05-19 06:04:47",
    "message": "Validation error",
    "subErrors": [
      {
        "object": "user",
        "field": "height",
        "rejectedValue": 500,
        "message": "用戶身高不能超過250釐米"
      },
      {
        "object": "user",
        "field": "age",
        "rejectedValue": 200,
        "message": "用戶年齡不能超過120歲"
      }
    ]
  }
}

如上所示,當API客戶端提供不正確的數據時,REST API返回了格式良好的異常提示信息,timestamp由原來的整形時間戳格式化爲一個可讀的日期+時間,同時還詳細列舉了詳細的錯誤報告。數據庫

2. 包裝異常信息

爲了可以提供一個可讀的JSON格式異常信息給API客戶端,咱們須要在項目中引入Jackson JSR 310的依賴包,使用其提供的@JsonFormat註解將Java中的日期和時間按照咱們給定的日期和時間模板進行格式化。如今,將下面的依賴包加入的Maven pom.xml文件中:json

<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-jsr310</artifactId>
    <version>2.9.8</version>
</dependency>

依賴就緒後,咱們須要提供一個異常信息的包裝類:RestApiError。它將負責對REST API拋出的異常信息進行封裝:api

public class RestApiError {

    private HttpStatus status;

    @JsonFormat(shape = JsonFormat.Shape.STRING,pattern = "yyyy-MM-dd hh:mm:ss")
    private LocalDateTime timestamp;

    private String message;

    private String debugMessage;

    private List<RestApiSubError> subErrors;

    private RestApiError(){
        timestamp = LocalDateTime.now();
    }

    RestApiError(HttpStatus status){
        this();
        this.status = status;
    }

    RestApiError(HttpStatus status,Throwable ex){
        this();
        this.status = status;
        this.message = "Unexpected error";
        this.debugMessage = ex.getLocalizedMessage();
    }

    RestApiError(HttpStatus status,String message,Throwable ex){
        this();
        this.status = status;
        this.message = message;
        this.debugMessage = ex.getLocalizedMessage();
    }
}
  • status 屬性用於記錄響應狀態。它沿用了HttpStatus的全部狀態嗎,如4xx和5xx。
  • timestamp屬性用於記錄發送錯誤的時間
  • message屬性用於記錄自定義的異常消息,一般是對API客戶端友好的提示信息
  • debugMessage屬性用於記錄更爲詳細的錯誤報告
  • subErrors屬性用於記錄異常附帶的子異常信息,如用戶實體中字段校驗信息等

RestApiSubError類用於記錄更爲細緻的異常信息,一般爲實體類中字段校驗失敗的異常報告:app

abstract class RestApiSubError{}
@Data
@EqualsAndHashCode(callSuper = false)
@AllArgsConstructor
class RestApiValidationError extends RestApiSubError{
    private String object;
    private String field;
    private Object rejectedValue;
    private String message;

    RestApiValidationError(String object,String message){
        this.object = object;
        this.message = message;
    }
}

RestApiSubError類是一個抽象的空類,具體的擴展將在RestApiValidationError中進行實現。RestApiValidationError類將記錄實體類中(如本講中的User對象)屬性校驗失敗報告。異步

如今,咱們來校驗GET /users/1 API,檢索用戶ID爲1的用戶信息:

{
  "id": 1,
  "username": "ramostear",
  "age": 28,
  "height": 170
}

REST API成功的返回了用戶信息,接下來咱們傳入一個系統不存在的用戶ID,看看REST API返回什麼信息:

GET /users/100

{
  "restapierror": {
    "status": "NOT_FOUND",
    "timestamp": "2019-05-19 06:31:17",
    "message": "User was not found for parameters {id=100}"
  }
}

經過上述的JSON信息咱們能夠看到,檢索不存在的用戶信息,REST API返回了友好的提示信息。在一開始的時候咱們測試提供不合符規範的用戶年齡和身高信息,接下來咱們在來測試一下提供一個空的用戶名,觀察REST API返回的信息:

{
  "restapierror": {
    "status": "BAD_REQUEST",
    "timestamp": "2019-05-19 06:37:46",
    "message": "Validation error",
    "subErrors": [
      {
        "object": "user",
        "field": "username",
        "rejectedValue": "",
        "message": "不能爲空"
      }
    ]
  }
}

3. Spring Boot 處理異常信息的流程

Spring Boot 處理REST API異常信息將會涉及到三個註解:

  • @RestController : 負責處理REST API具體操做邏輯的註解
  • @ExceptionHandler : 負責處理@RestController標註的類中拋出的異常的註解
  • @ControllerAdvice : 可以將@ExceptionHandler標註的方法集中到一個地方進行處理的註解

@ControllerAdivice註解是在Spring 3.2版本中新增的一個註解,它可以將單個由@ExceptionHandler註解標註的方法應用到多個控制器中。使用它的好處是咱們能夠在一個統一的地方同時處理多個控制器拋出的異常,當控制器有異常拋出時,ControllerAdvice會根據當前拋出的異常類型,自動匹配對應的ExceptionHandler;當沒有特定的Exception可用時,將調用默認的異常信息處理類來處理控制器拋出的異常(默認的異常信息處理類)。

下面,咱們經過一張流程示例圖,更爲直觀的瞭解Spring Application處理控制器異常信息的所有過程:

在圖中,藍色箭頭表示正常的請求和響應過程,紅色箭頭表示發生異常的請求和響應過程。

4. 自定義異常信息處理類

Spring Framework自帶的異常信息處理類每每不能知足咱們實際的業務需求,這就須要咱們定義符合具體狀況的異常信息處理類,在自定義異常信息處理類中,咱們能夠封裝更爲詳細的異常報告。

自定義異常信息處理類,咱們能夠站在「巨人」的肩膀上,快速封裝本身的異常信息處理類,而沒必要要從頭開始造「輪子」。如今,爲了快熟實現自定義異常信息處理類,並讓其正常工做,咱們能夠直接擴展Spring 提供的ResponseEntityExceptionHandler類來定義用戶異常信息處理類。ResponseEntityExceptionHandler已經提供了不少可用的功能,咱們只須要擴展該類或者覆蓋其提供的方法便可。

打開ResponseEntityExceptionHandler類,咱們能夠看到以下的源碼:

public abstract class ResponseEntityExceptionHandler {
    
 //不支持的HTTP請求方法異常信息處理方法
 protected ResponseEntity<Object> handleHttpRequestMethodNotSupported(...){...}
//不支持的HTTP媒體類型異常處理方法
 protected ResponseEntity<Object> handleHttpMediaTypeNotSupported(...){...}   
//不接受的HTTP媒體類型異常處方法
 protected ResponseEntity<Object> handleHttpMediaTypeNotAcceptable(...){...}
 //請求路徑參數缺失異常處方法
 protected ResponseEntity<Object> handleMissingPathVariable(...){...}   
 //缺乏servlet請求參數異常處理方法   
 protected ResponseEntity<Object> handleMissingServletRequestParameter(...){...}
 //servlet請求綁定異常
 protected ResponseEntity<Object> handleServletRequestBindingException(...){...}   
//不支持轉換
 protected ResponseEntity<Object> handleConversionNotSupported(...){...}
 //類型不匹配
 protected ResponseEntity<Object> handleTypeMismatch(...){...}
 //消息沒法檢索
 protected ResponseEntity<Object> handleHttpMessageNotReadable(...){...}
 //HTTP消息不可寫
 protected ResponseEntity<Object> handleHttpMessageNotWritable(...){...}
 //方法參數無效
 protected ResponseEntity<Object> handleMethodArgumentNotValid(...){...}
 //缺乏servlet請求部分
 protected ResponseEntity<Object> handleMissingServletRequestPart(...){...}
 //綁定異常
 protected ResponseEntity<Object> handleBindException(...){...}
 //沒有發現處理程序異常
 protected ResponseEntity<Object> handleNoHandlerFoundException(...){...}   
 //異步請求超時異常   
 @Nullable
 protected ResponseEntity<Object> handleAsyncRequestTimeoutException(...){...}
 //內部異常
 protected ResponseEntity<Object> handleExceptionInternal(...){...}       
}

咱們選擇性的覆蓋幾個經常使用的異常處理方法,並添加咱們自定義異常處理方法:

public class RestExceptionHandler extends ResponseEntityExceptionHandler {

    @ExceptionHandler(UserNotFoundException.class)
    protected ResponseEntity<Object> handleUserNotFound(UserNotFoundException ex){
        RestApiError apiError = new RestApiError(HttpStatus.NOT_FOUND);
        apiError.setMessage(ex.getMessage());
        return buildResponseEntity(apiError);
    }


    @Override
    protected ResponseEntity<Object> handleMissingServletRequestParameter(
            MissingServletRequestParameterException ex, HttpHeaders headers,
            HttpStatus status, WebRequest request) {
        String error = ex.getParameterName() + " parameter is missing";
        return buildResponseEntity(new RestApiError(BAD_REQUEST, error, ex));
    }



    @Override
    protected ResponseEntity<Object> handleHttpMediaTypeNotSupported(
            HttpMediaTypeNotSupportedException ex,
            HttpHeaders headers,
            HttpStatus status,
            WebRequest request) {
        StringBuilder builder = new StringBuilder();
        builder.append(ex.getContentType());
        builder.append(" media type is not supported. Supported media types are ");
        ex.getSupportedMediaTypes().forEach(t -> builder.append(t).append(", "));
        return buildResponseEntity(new RestApiError(HttpStatus.UNSUPPORTED_MEDIA_TYPE, builder.substring(0, builder.length() - 2), ex));
    }
    
    ...

}

UserNotFoundException類爲咱們自定義異常信息類,在執行GET /users/{userIds}或 GET /users請求時,若是數據庫中不存在該ID的記錄信息,將拋出UserNotFoundException異常信息,且將響應狀態碼設置爲NOT_FOUND。UserNotFoundException源碼以下:

public class UserNotFoundException extends Exception {

    public UserNotFoundException(Class clz,String...searchParams){
        super(UserNotFoundException.generateMessage(clz.getSimpleName(),toMap(String.class,String.class,searchParams)));
    }

    private static String generateMessage(String entity, Map<String,String> searchParams){
        return StringUtils.capitalize(entity)+
                " was not found for parameters "+
                searchParams;
    }

    private static <K,V> Map<K,V> toMap(Class<K> key,Class<V> value,Object...entries){
        if(entries.length % 2 == 1){
            throw new IllegalArgumentException("Invalid entries");
        }
        return IntStream.range(0,entries.length/2).map(i->i*2)
                .collect(HashMap::new,
                        (m,i)->m.put(key.cast(entries[i]),value.cast(entries[i+1])),Map::putAll);
    }
}

下圖將更爲直觀的說明自定義異常處理的整個流程:

當UserService發生異常時,異常信息將向上傳遞到UserController,此時的異常信息被Spring所捕獲,並將其跳轉到UserNotFoundException處理方法中。UserNotFoundException將異常報告封裝到RestApiError對象中,並回傳給API Client。經過此方法,API客戶端將得到一份邏輯清晰的響應報告。

本次課程的所有源碼已經上傳到 Github 倉庫,你能夠點擊此連接獲取源碼: https://github.com/ramostear/...

原文:https://www.ramostear.com/art...

相關文章
相關標籤/搜索