Spring Boot 中自定義 WebFlux 異常

REST,Spring WebFlux
Remote
1
04:31 AM · Dec 01 ,2025

1. 簡介

在本教程中,我們將探索 Spring 框架中不同的錯誤響應格式。我們還將瞭解如何引發和處理 RFC7807 ProblemDetail 並帶有自定義屬性,以及如何在 Spring WebFlux 中引發自定義異常。

2. 異常響應格式在 Spring Boot 3

讓我們理解支持的各種錯誤響應格式。

默認情況下,Spring 框架提供 DefaultErrorAttributes 類,該類實現了 ErrorAttributes 接口,用於在未處理的錯誤發生時生成錯誤響應。對於默認錯誤,系統會生成一個 JSON 響應結構,供我們更仔細地檢查:

{
    "timestamp": "2023-04-01T00:00:00.000+00:00",
    "status": 500,
    "error": "Internal Server Error",
    "path": "/api/example"
}

雖然此錯誤響應包含一些關鍵屬性,但可能對調查問題不具幫助。幸運的是,我們可以通過在 Spring WebFlux 應用程序中創建 ErrorAttributes 接口的自定義實現來修改此默認行為。

從 Spring Framework 6 開始,ProblemDetail, 符合 RFC7807 規範的表示已得到支持。ProblemDetail 包含一些標準屬性,用於定義錯誤詳情,以及自定義詳情的選項。 相關的屬性列表如下:

  • type (字符串) – 標識問題的 URI 引用
  • title (字符串) – 問題的簡短摘要
  • status (數字) – HTTP 狀態碼
  • detail (字符串) – 應包含異常的詳細信息。
  • instance (字符串) – 用於標識問題原因的 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 的名稱。此異常封裝一個 ProblemDetail 對象,該對象生成有關發生的錯誤的一些額外信息。我們可以擴展此異常以自定義並添加屬性。

3. 如何實現 ProblemDetail RFC 7807 異常

Spring 6+ / Spring Boot 3+ 應用默認支持 ProblemDetail 異常,但我們需要通過以下方式之一啓用它。

3.1. 通過 Properties 文件啓用 ProblemDetail 異常

可以通過添加以下屬性來啓用 ProblemDetail 異常:

spring:
  mvc:
    problemdetails:
      enabled: true

3.2. 通過添加 Exception Handler 啓用 ProblemDetail 異常

還可以通過擴展 ResponseEntityExceptionHandler 並添加自定義異常處理器(即使沒有覆蓋任何方法):

@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 GlobalExceptionHandler 自定義處理器,ProblemDetail 變為默認響應格式。為了測試這一點,我們可以嘗試訪問未支持的 HTTP 方法,例如 POST,以查看異常格式。

當拋出 MethodNotAllowedException 時,ResponseEntityExceptionHandler 將處理異常並以 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 類中提供一個錯誤處理器。此處理器應設置 API_USER_NOT_FOUND 屬性的 ErrorDetails 對象,以及 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;
}

我們需要創建一個 ErrorDetailsSerializerProblemDetailsSerializer 以自定義響應格式。

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();
    }
}

ProblemDetailsSerializer 負責格式化整體 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"
        }
    ]
}

我們可以使用 ErrorResponseException 來暴露 HTTP 狀態、響應頭和包含 RFC 7807 ProblemDetail 協議的響應體。

在這些示例中,我們使用 GlobalExceptionHandler 來處理全局異常。 也可以使用 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"
        }
    ]
}

6. 結論

在本文中,我們探討了如何啓用和使用Spring Framework提供的RFC7807異常格式,並學習瞭如何在Spring WebFlux中創建和處理自定義異常。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.