程序員總結:Spring Boot REST API錯誤處理指南

程序員 JSON Java Line 孝道的重要性 孝道的重要性 2017-09-08

API在提供錯誤消息的同時進行適當的錯誤處理,是一個非常有用的功能,因為這能讓API客戶端對問題進行正確地響應。API處理錯誤的默認行為通常是返回難以理解的堆棧跟蹤,而這些對API客戶端來說並沒有什麼用。將錯誤信息切分成多個字段可以方便API客戶端的解析,以此向用戶提供更加友好的錯誤消息。本文將介紹在使用Spring Boot構建REST API的時候如何進行合適的錯誤處理。

這裡還是要推薦下我自己建的前端學習群:657137906,如果你正在學習前端,小編歡迎你加入,大家都是前端黨,不定期分享乾貨(只有web前端相關的),包括我自己整理的一份2017最新的前端資料和零基礎入門教程,歡迎初學和進階中的小夥伴。

程序員總結:Spring Boot REST API錯誤處理指南

在過去幾年裡,使用Spring構建REST API已經成為Java開發人員的標準方法。而使用Spring Boot則有助於API的構建,因為它刪除了大量的樣板代碼,並實現了各種組件的自動化配置。我們假設你對利用這些技術進行API開發的基礎知識已經非常瞭解。如果你對如何開發基本的REST API並不熟悉,那麼你應該先閱讀這篇關於Spring MVC的文章或另一篇有關構建Spring REST服務的文章。

讓錯誤響應更清晰

在本文中,我們將實現一個通過REST API來檢索鳥類(代表一個對象)的應用程序,代碼託管在GitHub上。這個示例包含了本文描述的所有功能,以及比較多的錯誤處理場景。以下是該程序實現的端點URL:

GET /birds/{birdId}獲取鳥的相關信息,如果沒有找到,則拋出異常。
GET /birds/noexception/{birdId}這個調用也可以獲取鳥的相關信息,但是即使沒有找到相應的鳥,也不會拋出異常。
POST /birds創建一隻鳥。

Spring框架的MVC模塊在錯誤處理方面提供了一些很不錯的功能,但是這些功能需要由開發人員主動調用,才能返回對API客戶端的有具體意義的響應。

我們來看一下這個Spring Boot默認響應的例子。當我們向/birds發送一個HTTP POST的時候,消息內容是下面這個JSON對象,字段“mass”的值是字符串“aaa”,這個字段本應該填一個整數:

{"scientificName": "Common blackbird","specie": "Turdus merula","mass": "aaa","length": 4}

Spring Boot的默認響應,沒有正確的處理錯誤:

{ "timestamp": 1500597044204, "status": 400, "error": "Bad Request", "exception": "org.springframework.http.converter.HttpMessageNotReadableException", "message": "JSON parse error: Unrecognized token 'three': was expecting ('true', 'false' or 'null'); nested exception is com.fasterxml.jackson.core.JsonParseException: Unrecognized token 'aaa': was expecting ('true', 'false' or 'null')\n at [Source: java.io.PushbackInputStream@cba7ebc; line: 4, column: 17]","path": "/birds"}

呃…… 響應消息裡有一些很有用的字段,但它裡面有關異常的內容太多了。 順便說一句,這是Spring Boot中DefaultErrorAttributes類的內容。 timestamp字段是一個整數,不攜帶什麼度量單位的時間戳信息。exception字段只有Java開發人員會感興趣,該消息使API消費者迷失在與它們無關的細節中。是否有更多的細節可以從錯誤產生的異常中提取出來呢? 下面,我們來學習如何正確地處理這些異常,並將它們包裝成更好的JSON表示形式,讓API客戶端更容易識別。

由於我們要使用Java 8的日期和時間類,因此首先需要為Jackson JSR310轉換器添加一個Maven依賴關係。這個包使用註解@JsonFormat將Java 8的日期和時間類轉換為JSON:

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

好的,我們來定義一個表示API錯誤的類。 我們將創建一個名為ApiError的類,該類用於保存REST調用期間發生錯誤的相關信息。

class ApiError { private HttpStatus status; @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd-MM-yyyy hh:mm:ss") private LocalDateTime timestamp; private String message; private String debugMessage; private List<ApiSubError> subErrors; private ApiError() { timestamp = LocalDateTime.now(); } ApiError(HttpStatus status) { this(); this.status = status; } ApiError(HttpStatus status, Throwable ex) { this(); this.status = status; this.message = "Unexpected error"; this.debugMessage = ex.getLocalizedMessage(); } ApiError(HttpStatus status, String message, Throwable ex) { this(); this.status = status; this.message = message; this.debugMessage = ex.getLocalizedMessage(); }}
  • status屬性保存了操作調用的狀態。 比如,4xx表示客戶端錯誤,5xx意味著服務器錯誤。 比較常見的情況是:http返回碼400表示BAD_REQUEST,例如,客戶端發送了格式不正確的字段(如無效的電子郵件地址)。

  • timestamp屬性保存了發生錯誤的日期時間。

  • message屬性保存了對用戶友好的錯誤信息。

  • debugMessage屬性更詳細地描述了錯誤。

  • subErrors屬性保存了發生的子錯誤的數組。 這用於表示在單個調用中出現的多個錯誤。比如,校驗的時候有多個字段驗證失敗。用ApiSubError類進行封裝。

abstract class ApiSubError {}@Data@EqualsAndHashCode(callSuper = false)@AllArgsConstructorclass ApiValidationError extends ApiSubError { private String object; private String field; private Object rejectedValue; private String message; ApiValidationError(String object, String message) { this.object = object; this.message = message; }}

ApiValidationError是類ApiSubError的擴展類,表示REST調用時遇到的校驗問題。

下面,你將看到幾個JSON響應的例子,這些響應根據我們上面的描述做了改進。

以下這個JSON是在調用URLGET /birds/2後找不到實體的時候返回的:

{"apierror": { "status": "NOT_FOUND", "timestamp": "18-07-2017 06:20:19", "message": "Bird was not found for parameters {id=2}"}}

下面是調用POST /birds時傳入了無效值後返回的JSON示例:

{"apierror": { "status": "BAD_REQUEST", "timestamp": "18-07-2017 06:49:25", "message": "Validation errors", "subErrors": [ { "object": "bird", "field": "mass", "rejectedValue": 999999, "message": "must be less or equal to 104000" } ] }}

Spring Boot 錯誤處理

我們來探討一些用於異常處理的Spring註解。

RestController是用於REST操作類的最基本的註解。

ExceptionHandler這個Spring註解提供了一種機制,用來處理在執行程序期間拋出的異常。此註解將作為處理此控制器中拋出的異常的入口點。總而言之,最常見的方法是在@ControllerAdvice類的方法上使用@ExceptionHandler,以便將異常處理應用於全局或控制器的子集。

ControllerAdvice是Spring 3.2中引入的註解,顧名思義,它是多控制器的“建議”。它使得單個ExceptionHandler應用於多個控制器上。這樣我們可以在一個地方定義如何處理這樣的異常,當ControllerAdvice覆蓋的類拋出異常時,這個處理程序就會被調用。受影響的控制器子集可以在@ControllerAdvice上使用以下選擇器進行定義:annotations()basePackageClasses()basePackages()。如果沒有提供選擇器,則ControllerAdvice將應用於全局所有的控制器。

所以,通過使用@ExceptionHandler@ControllerAdvice,我們可以定義一個用於處理異常的中心點,並將異常包裝在ApiError對象中,這比Spring Boot默認的錯誤處理機制更好。

處理異常

程序員總結:Spring Boot REST API錯誤處理指南

下一步是創建處理異常的類。為了簡單起見,我們稱之為RestExceptionHandler,它必須繼承自Spring Boot的ResponseEntityExceptionHandler。我們也將從ResponseEntityExceptionHandler繼承,因為它已經提供了對Spring MVC異常的一些基本處理方法,所以,我們將改進現有的異常處理手段,並同時添加針對新異常的處理。

如果看一下ResponseEntityExceptionHandler的源代碼,你會看到有很多方法名為handle******(),像handleHttpMessageNotReadable()handleHttpMessageNotWritable()。我們來看看如何對handleHttpMessageNotReadable()進行擴展來處理HttpMessageNotReadableException異常。我們只需要在RestExceptionHandler類中重寫方法handleHttpMessageNotReadable()

@Order(Ordered.HIGHEST_PRECEDENCE)@ControllerAdvicepublic class RestExceptionHandler extends ResponseEntityExceptionHandler { @Override protected ResponseEntity<Object> handleHttpMessageNotReadable(HttpMessageNotReadableException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { String error = "Malformed JSON request"; return buildResponseEntity(new ApiError(HttpStatus.BAD_REQUEST, error, ex)); } private ResponseEntity<Object> buildResponseEntity(ApiError apiError) { return new ResponseEntity<>(apiError, apiError.getStatus()); } //other exception handlers below}

如果拋出一個HttpMessageNotReadableException,則錯誤消息將是“Malformed JSON request(格式錯誤的JSON請求)”,該錯誤封裝在ApiError對象內。下面我們可以看到新的應答:

{ "apierror": { "status": "BAD_REQUEST", "timestamp": "21-07-2017 03:53:39", "message": "Malformed JSON request", "debugMessage": "JSON parse error: Unrecognized token 'aaa': was expecting ('true', 'false' or 'null'); nested exception is com.fasterxml.jackson.core.JsonParseException: Unrecognized token 'aaa': was expecting ('true', 'false' or 'null')\n at [Source: java.io.PushbackInputStream@7b5e8d8a; line: 4, column: 17]"}}

處理自定義異常

現在,我們來看看如何創建一個方法來處理沒有在Spring Boot的ResponseEntityExceptionHandler中聲明的異常。

Spring程序處理數據庫調用的一個常見場景是使用庫類通過id去查找記錄。但是,如果研究一下CrudRepository.findOne()方法,我們會發現,如果找不到對象,它將返回null。這意味著如果我們的服務只是調用這個方法並直接返回給控制器,那麼即使找不到資源,我們也會得到HTTP返回碼200(OK)。實際上,正確的方法是返回HTTP/1.1規範中指定的HTTP返回碼404(NOT FOUND)。

為了處理這種情況,我們將創建一個名為EntityNotFoundException的自定義異常。它與javax.persistence.EntityNotFoundException不同,因為它提供的一些構造函數可以用來選擇以不同的方式處理javax.persistence異常。

程序員總結:Spring Boot REST API錯誤處理指南

也就是說,我們可以在RestExceptionHandler類中為這個新創建的EntityNotFoundException創建一個ExceptionHandler。為此,創建一個名為handleEntityNotFound()的方法,並使用@ExceptionHandler對其進行註釋,將類對象EntityNotFoundException.class傳遞給它。這表示每次拋出EntityNotFoundException的時候,Spring應該調用此方法來處理它。當用@ExceptionHandler註釋一個方法時,它將接受各種自動注入的參數,如WebRequestLocale,以及在這裡提到的其他參數。我們將提供異常EntityNotFoundException本身作為handleEntityNotFound方法的參數。

@Order(Ordered.HIGHEST_PRECEDENCE)@ControllerAdvicepublic class RestExceptionHandler extends ResponseEntityExceptionHandler { //other exception handlers @ExceptionHandler(EntityNotFoundException.class) protected ResponseEntity<Object> handleEntityNotFound( EntityNotFoundException ex) { ApiError apiError = new ApiError(NOT_FOUND); apiError.setMessage(ex.getMessage()); return buildResponseEntity(apiError); }}

太好了!我們在handleEntityNotFound()方法裡將HTTP狀態代碼設置為NOT_FOUND,並使用了新的異常消息。以下是GET /birds/2的響應示例:

{"apierror": { "status": "NOT_FOUND", "timestamp": "21-07-2017 04:02:22", "message": "Bird was not found for parameters {id=2}"}}

結論

對異常處理的控制非常重要,所以我們需要將這些異常正確映射到ApiError

最後在說幾句:

  1. 厲害程序員相對於普通程序員的優勢在於:

  2. 寫出的代碼更容易排錯,不是高手的代碼就不會錯,而是高手的代碼出了錯容易找。高手的代碼可讀性一定很好,模塊清晰,命名規範,格式工整,關鍵的地方有註釋,出了異常有log,自然容易排錯,即使交給別人去debug也是比較容易的。

  3. 這個話題到這裡就算是說完了,學習web前端的可以加我的群,每天分享對應的學習資料:657137906,歡迎初學和進階中的小夥伴。

如果想看到更加系統的文章和學習方法經驗可以關注我的微信公眾號:‘前端根據地’關注後回覆‘給我資料’可以領取一套完整的學習視頻

相關推薦

推薦中...