Spring Boot REST API 錯誤處理指南
已發表: 2022-03-11在提供有意義的錯誤消息的同時正確處理 API 中的錯誤是一個非常理想的功能,因為它可以幫助 API 客戶端正確響應問題。 默認行為往往是返回難以理解且最終對 API 客戶端無用的堆棧跟踪。 將錯誤信息劃分為字段還使 API 客戶端能夠解析它並向用戶提供更好的錯誤消息。 在本文中,我們將介紹在使用 Spring Boot 構建 REST API 時如何進行正確的錯誤處理。
在過去的幾年裡,使用 Spring 構建 REST API 成為 Java 開發人員的標準方法。 使用 Spring Boot 有很大幫助,因為它刪除了許多樣板代碼並啟用了各種組件的自動配置。 在應用此處描述的知識之前,我們將假設您熟悉使用這些技術進行 API 開發的基礎知識。 如果您仍然不確定如何開發基本的 REST API,那麼您應該從這篇關於 Spring MVC 的文章或另一篇關於構建 Spring REST 服務的文章開始。
使錯誤響應更清晰
在本文中,我們將使用託管在 GitHub 上的應用程序的源代碼,該應用程序實現 REST API 以檢索代表鳥類的對象。 它具有本文中描述的功能以及更多錯誤處理場景的示例。 以下是該應用程序中實現的端點的摘要:
GET /birds/{birdId} | 獲取有關鳥的信息,如果未找到則拋出異常。 |
GET /birds/noexception/{birdId} | 此調用還獲取有關鳥的信息,除非在未找到鳥的情況下不會引發異常。 | POST /birds | 創造一隻鳥。 | </tr>
Spring 框架 MVC 模塊帶有一些很棒的特性來幫助處理錯誤。 但是留給開發人員使用這些功能來處理異常並向 API 客戶端返回有意義的響應。
讓我們看一個默認 Spring Boot 答案的示例,當我們使用以下 JSON 對象向/birds
端點發出 HTTP POST 時,該對像在字段“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) @AllArgsConstructor class 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 響應的一些示例,只是為了了解我們在本文結束時將擁有的內容。
以下是在調用端點GET /birds/2
時未找到實體時返回的 JSON 示例:
{ "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 中引入的註解,顧名思義,就是多個控制器的“Advice”。 它用於使單個ExceptionHandler
應用於多個控制器。 這樣我們就可以在一個地方定義如何處理這樣的異常,並且當這個ControllerAdvice
覆蓋的類拋出異常時,這個處理程序將被調用。 受影響的控制器子集可以通過在@ControllerAdvice
上使用以下選擇器來定義: annotations()
、 basePackageClasses()
和basePackages()
。 如果沒有提供選擇器,則ControllerAdvice
將全局應用於所有控制器。
因此,通過使用@ExceptionHandler
和@ControllerAdvice
,我們將能夠定義一個中心點來處理異常並將它們包裝在一個ApiError
對像中,該對像比默認的 Spring Boot 錯誤處理機制具有更好的組織。
處理異常
下一步是創建將處理異常的類。 為簡單起見,我們稱它為RestExceptionHandler
,它必須從 Spring Boot 的ResponseEntityExceptionHandler
擴展而來。 我們將擴展ResponseEntityExceptionHandler
,因為它已經提供了 Spring MVC 異常的一些基本處理,因此我們將為新異常添加處理程序,同時改進現有異常。
覆蓋在 ResponseEntityExceptionHandler 中處理的異常
如果您查看ResponseEntityExceptionHandler
的源代碼,您會看到很多名為handle******()
的方法,例如handleHttpMessageNotReadable()
或handleHttpMessageNotWritable()
。 我們先看看如何擴展handleHttpMessageNotReadable()
來處理HttpMessageNotReadableException
異常。 我們只需要在我們的RestExceptionHandler
類中重寫方法handleHttpMessageNotReadable()
:
@Order(Ordered.HIGHEST_PRECEDENCE) @ControllerAdvice public 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”,並且錯誤將被封裝在ApiError
對像中。 下面我們可以看到重寫了這個新方法的 REST 調用的答案:
{ "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
異常。
也就是說,讓我們在RestExceptionHandler
類中為這個新創建的EntityNotFoundException
創建一個ExceptionHandler
。 為此,創建一個名為handleEntityNotFound()
的方法並使用@ExceptionHandler
對其進行註釋,並將類對象EntityNotFoundException.class
傳遞給它。 這表明 Spring 每次拋出EntityNotFoundException
時,Spring 都應該調用這個方法來處理它。 使用@ExceptionHandler
註釋方法時,它將接受各種自動注入的參數,如WebRequest
、 Locale
和此處所述的其他參數。 我們將只提供異常EntityNotFoundException
本身作為此handleEntityNotFound
方法的參數。
@Order(Ordered.HIGHEST_PRECEDENCE) @ControllerAdvice public 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
對象,並提供重要信息,讓 API 客戶端知道發生了什麼。 下一步是為應用程序代碼中拋出的異常創建更多處理程序方法(帶有@ExceptionHandler 的方法)。 GitHub 代碼中還有一些其他常見異常的示例,例如MethodArgumentTypeMismatchException
、 ConstraintViolationException
等。
以下是有助於撰寫本文的一些其他資源:
Baeldung - 使用 Spring 對 REST 進行錯誤處理
Spring 博客 - Spring MVC 中的異常處理