API在提供錯誤消息的同時進行適當的錯誤處理,是一個非常有用的功能,因為這能讓API客戶端對問題進行正確地響應。API處理錯誤的默認行為通常是返回難以理解的堆棧跟蹤,而這些對API客戶端來說並沒有什麼用。將錯誤信息切分成多個字段可以方便API客戶端的解析,以此向用戶提供更加友好的錯誤消息。本文將介紹在使用Spring Boot構建REST API的時候如何進行合適的錯誤處理。
這裡還是要推薦下我自己建的前端學習群:657137906,如果你正在學習前端,小編歡迎你加入,大家都是前端黨,不定期分享乾貨(只有web前端相關的),包括我自己整理的一份2017最新的前端資料和零基礎入門教程,歡迎初學和進階中的小夥伴。
在過去幾年裡,使用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默認的錯誤處理機制更好。
處理異常
下一步是創建處理異常的類。為了簡單起見,我們稱之為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
異常。
也就是說,我們可以在RestExceptionHandler
類中為這個新創建的EntityNotFoundException
創建一個ExceptionHandler
。為此,創建一個名為handleEntityNotFound()
的方法,並使用@ExceptionHandler
對其進行註釋,將類對象EntityNotFoundException.class
傳遞給它。這表示每次拋出EntityNotFoundException
的時候,Spring應該調用此方法來處理它。當用@ExceptionHandler
註釋一個方法時,它將接受各種自動注入的參數,如WebRequest
、Locale
,以及在這裡提到的其他參數。我們將提供異常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
最後在說幾句:
厲害程序員相對於普通程序員的優勢在於:
寫出的代碼更容易排錯,不是高手的代碼就不會錯,而是高手的代碼出了錯容易找。高手的代碼可讀性一定很好,模塊清晰,命名規範,格式工整,關鍵的地方有註釋,出了異常有log,自然容易排錯,即使交給別人去debug也是比較容易的。
這個話題到這裡就算是說完了,學習web前端的可以加我的群,每天分享對應的學習資料:657137906,歡迎初學和進階中的小夥伴。