Spring Boot 3 中的自定義 WebFlux 異常
一、簡介
在本教程中,我們將探索 Spring 框架中的不同錯誤響應格式。我們還將了解如何使用自定義屬性引發和處理RFC7807 ProblemDetail
,以及如何在 Spring WebFlux 中引發自定義異常。
二、Spring Boot 3中的異常響應格式
讓我們了解開箱即用支持的各種錯誤響應格式。
默認情況下,Spring Framework 提供了[DefaultErrorAttributes](https://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/web/servlet/error/DefaultErrorAttributes.html)
類,該類實現了[ErrorAttributes](https://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/web/servlet/error/ErrorAttributes.html)
接口,以在發生未處理的錯誤時生成錯誤響應。在默認錯誤的情況下,系統會生成一個 JSON 響應結構,我們可以更仔細地檢查它:
{
"timestamp": "2023-04-01T00:00:00.000+00:00",
"status": 500,
"error": "Internal Server Error",
"path": "/api/example"
}
雖然此錯誤響應包含一些關鍵屬性,但它可能無助於調查問題。幸運的是,我們可以通過在 Spring WebFlux 應用程序中創建[ErrorAttributes](https://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/web/servlet/error/ErrorAttributes.html)
接口的自定義實現來修改此默認行為。
從 Spring Framework 6 [ProblemDetail](https://docs.spring.io/spring-framework/docs/6.0.7/reference/html/web.html#mvc-ann-rest-exceptions) ,
支持RFC7807規範的表示。 ProblemDetail
包括一些定義錯誤詳細信息的標準屬性,還有一個擴展詳細信息以進行自定義的選項。下面列出了支持的屬性:
-
type
(string) – 標識問題類型的 URI 引用 -
title
(string) – 問題類型的簡短摘要 -
status
(number) – HTTP 狀態代碼 -
detail
(string) – 應該包含異常的詳細信息。 -
instance
(string) – 一個 URI 引用,用於識別問題的具體原因。例如,它可以指導致問題的屬性。
除了上面提到的標準屬性外, ProblemDetail
還包含一個Map<String, Object>
以添加自定義參數以提供有關問題的更多詳細信息。
讓我們看一下帶有自定義對象errors
示例錯誤響應結構:
{
"type": "https://example.com/probs/email-invalid",
"title": "Invalid email address",
"detail": "The email address 'john.doe' is invalid.",
"status": 400,
"timestamp": "2023-04-07T12:34:56.789Z",
"errors": [
{
"code": "123",
"message": "Error message",
"reference": "https//error/details#123"
}
]
}
Spring Framework 還提供了一個名為[ErrorResponseException](https://docs.spring.io/spring-framework/docs/current/javadoc-api//org/springframework/web/ErrorResponseException.html)
基本實現。此異常封裝了一個ProblemDetail
對象,該對像生成有關發生的錯誤的附加信息。我們可以擴展這個異常來自定義和添加屬性。
3. ProblemDetail
RFC 7807異常如何實現
雖然 Spring 6+ / Spring Boot 3+ 應用程序默認支持ProblemDetail
異常,但我們需要通過以下方式之一啟用它。
3.1.通過屬性文件啟用ProblemDetail
異常
可以通過添加屬性來啟用ProblemDetail
異常:
spring:
mvc:
problemdetails:
enabled: true
3.2.通過添加異常處理程序啟用ProblemDetail
異常
ProblemDetail
異常也可以通過擴展[ResponseEntityExceptionHandler](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandler.html)
並添加自定義異常處理程序(即使沒有任何覆蓋)來啟用:
@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
//...
}
我們將在本文中使用這種方法,因為我們需要添加自定義異常處理程序。
3.3.實施ProblemDetail
異常
讓我們通過考慮一個簡單的應用程序來檢查如何使用自定義屬性引發和處理ProblemDetail
異常,該應用程序提供了一些用於創建和檢索User
信息的端點。
我們的控制器有一個GET /v1/users/{userId}
端點,它根據提供的userId
檢索用戶信息。如果找不到任何記錄,代碼將拋出一個簡單的自定義異常,稱為UserNotFoundException
:
@GetMapping("/v1/users/{userId}")
public Mono<ResponseEntity<User>> getUserById(@PathVariable Long userId) {
return Mono.fromCallable(() -> {
User user = userMap.get(userId);
if (user == null) {
throw new UserNotFoundException("User not found with ID: " + userId);
}
return new ResponseEntity<>(user, HttpStatus.OK);
});
}
我們的UserNotFoundException
擴展了RunTimeException
:
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(String message) {
super(message);
}
}
由於我們有一個擴展[ResponseEntityExceptionHandler](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandler.html)
GlobalExceptionHandler
自定義處理程序,因此ProblemDetail
成為默認的異常格式。為了測試這一點,我們可以嘗試使用不受支持的 HTTP 方法訪問應用程序,例如 POST,以查看異常格式。
當拋出MethodNotAllowedException
時, [ResponseEntityExceptionHandler](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandler.html)
將處理異常並生成ProblemDetail
格式的響應:
curl --location --request POST 'localhost:8080/v1/users/1'
這導致ProblemDetail
對像作為響應:
{
"type": "about:blank",
"title": "Method Not Allowed",
"status": 405,
"detail": "Supported methods: [GET]",
"instance": "/users/1"
}
3.4.在 Spring WebFlux 中使用自定義屬性擴展ProblemDetail
異常
讓我們通過為UserNotFoundException
提供異常處理程序來擴展該示例,該異常處理程序將自定義對象添加到ProblemDetail
響應。
ProblemDetail
對象包含一個properties
屬性,該屬性接受String
作為鍵和值作為任何Object
。
我們將添加一個名為ErrorDetails
的自定義對象。此對象包含錯誤代碼和消息,以及錯誤參考 URL,其中包含用於解決問題的其他詳細信息和說明:
@JsonSerialize(using = ErrorDetailsSerializer.class)
public enum ErrorDetails {
API_USER_NOT_FOUND(123, "User not found", "http://example.com/123");
@Getter
private Integer errorCode;
@Getter
private String errorMessage;
@Getter
private String referenceUrl;
ErrorDetails(Integer errorCode, String errorMessage, String referenceUrl) {
this.errorCode = errorCode;
this.errorMessage = errorMessage;
this.referenceUrl = referenceUrl;
}
}
要覆蓋UserNotException
的錯誤行為,我們需要在GlobalExceptionHandler
類中提供錯誤處理程序。此處理程序應設置ErrorDetails
對象的API_USER_NOT_FOUND
屬性,以及ProblemDetail
對象提供的任何其他錯誤詳細信息:
@ExceptionHandler(UserNotFoundException.class)
protected ProblemDetail handleNotFound(RuntimeException ex) {
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
problemDetail.setTitle("User not found");
problemDetail.setType(URI.create("https://example.com/problems/user-not-found"));
problemDetail.setProperty("errors", List.of(ErrorDetails.API_USER_NOT_FOUND));
return problemDetail;
}
我們還需要一個ErrorDetailsSerializer
和ProblemDetailSerializer
來自定義響應格式。
ErrorDetailsSerializer
負責使用錯誤代碼、錯誤消息和參考詳細信息來格式化我們的自定義錯誤對象:
public class ErrorDetailsSerializer extends JsonSerializer<ErrorDetails> {
@Override
public void serialize(ErrorDetails value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeStartObject();
gen.writeStringField("code", value.getErrorCode().toString());
gen.writeStringField("message", value.getErrorMessage());
gen.writeStringField("reference", value.getReferenceUrl());
gen.writeEndObject();
}
}
ProblemDetailSerializer
負責格式化整個ProblemDetail
對像以及自定義對象(在ErrorDetailsSerializer
的幫助下):
public class ProblemDetailsSerializer extends JsonSerializer<ProblemDetail> {
@Override
public void serialize(ProblemDetail value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeStartObject();
gen.writeObjectField("type", value.getType());
gen.writeObjectField("title", value.getTitle());
gen.writeObjectField("status", value.getStatus());
gen.writeObjectField("detail", value.getDetail());
gen.writeObjectField("instance", value.getInstance());
gen.writeObjectField("errors", value.getProperties().get("errors"));
gen.writeEndObject();
}
}
現在,當我們嘗試使用無效的userId
訪問端點時,我們應該會收到一條帶有自定義屬性的錯誤消息:
$ curl --location 'localhost:8080/v1/users/1'
這會產生ProblemDetail
對像以及自定義屬性:
{
"type": "https://example.com/problems/user-not-found",
"title": "User not found",
"status": 404,
"detail": "User not found with ID: 1",
"instance": "/users/1",
"errors": [
{
"errorCode": 123,
"errorMessage": "User not found",
"referenceUrl": "http://example.com/123"
}
]
}
我們還可以使用實現ErrorResponse
的ErrorResponseException
來公開 HTTP 狀態、響應標頭和正文以及 RFC 7807 ProblemDetail
的約定。
在這些示例中,我們使用[ResponseEntityExceptionHandler](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandler.html) .
或者,也可以使用AbstractErrorWebExceptionHandler
來處理全局 Webflux 異常。
4. 為什麼要自定義異常
雖然ProblemDetail
格式對於添加自定義屬性很有幫助且靈活,但在某些情況下,我們可能更願意拋出一個包含錯誤所有詳細信息的自定義錯誤對象。在這種情況下,在 Spring 中使用自定義異常可以提供一種清晰、更具體、更一致的方法來處理代碼中的錯誤和異常。
5.在Spring WebFlux中實現自定義異常
讓我們考慮實現一個自定義對像作為響應而不是ProblemDetail
:
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class CustomErrorResponse {
private String traceId;
private OffsetDateTime timestamp;
private HttpStatus status;
private List<ErrorDetails> errors;
}
要拋出這個自定義對象,我們需要一個自定義異常:
public class CustomErrorException extends RuntimeException {
@Getter
private CustomErrorResponse errorResponse;
public CustomErrorException(String message, CustomErrorResponse errorResponse) {
super(message);
this.errorResponse = errorResponse;
}
}
讓我們創建另一個版本,端點的v2
,它拋出這個自定義異常。為簡單起見,某些字段(如traceId,
填充了隨機值:
@GetMapping("/v2/users/{userId}")
public Mono<ResponseEntity<User>> getV2UserById(@PathVariable Long userId) {
return Mono.fromCallable(() -> {
User user = userMap.get(userId);
if (user == null) {
CustomErrorResponse customErrorResponse = CustomErrorResponse
.builder()
.traceId(UUID.randomUUID().toString())
.timestamp(OffsetDateTime.now().now())
.status(HttpStatus.NOT_FOUND)
.errors(List.of(ErrorDetails.API_USER_NOT_FOUND))
.build();
throw new CustomErrorException("User not found", customErrorResponse);
}
return new ResponseEntity<>(user, HttpStatus.OK);
});
}
我們需要在GlobalExceptionHandler
中添加一個處理程序來最終格式化輸出響應中的異常:
@ExceptionHandler({CustomErrorException.class})
protected ResponseEntity<CustomErrorResponse> handleCustomError(RuntimeException ex) {
CustomErrorException customErrorException = (CustomErrorException) ex;
return ResponseEntity.status(customErrorException.getErrorResponse().getStatus()).body(customErrorException.getErrorResponse());
}
現在,如果我們嘗試使用無效的userId,
我們應該會收到帶有自定義屬性的錯誤:
$ curl --location 'localhost:8080/v2/users/1'
這導致CustomErrorResponse
對像作為響應:
{
"traceId": "e3853069-095d-4516-8831-5c7cfa124813",
"timestamp": "2023-04-28T15:36:41.658289Z",
"status": "NOT_FOUND",
"errors": [
{
"code": "123",
"message": "User not found",
"reference": "http://example.com/123"
}
]
}
六,結論
在本文中,我們探討瞭如何啟用和使用Spring Framework 提供的ProblemDetail
RFC7807 異常格式,並學習瞭如何在 Spring WebFlux 中創建和處理自定義異常。
與往常一樣,這些示例的源代碼可在 GitHub 上獲得。