知識庫 / Spring WebFlux RSS 訂閱

使用 Spring WebClient 定製 JSON 反序列化

JSON,Spring WebFlux
HongKong
8
09:44 PM · Dec 05 ,2025

1. 概述

本文將探討自定義反序列化的必要性,以及如何使用 Spring WebClient 實現它。

2. 為什麼我們需要自定義反序列化?

Spring WebClient 在 Spring WebFlux 模塊中通過 EncoderDecoder 組件處理序列化和反序列化。 EncoderDecoder 作為接口存在,代表了讀取和寫入內容之間的合同。 默認情況下, spring-core 模塊提供 byte[]、ByteBufferDataBufferResourceString 編碼器和解碼器實現。

Jackson 是一個庫,它通過 ObjectMapper 暴露助手實用程序,將 Java 對象序列化為 JSON,並將 JSON 字符串反序列化為 Java 對象。 ObjectMapper 包含內置配置,可以使用 反序列化 功能進行啓用/禁用。

當 Jackson 庫提供的默認行為無法滿足我們的特定要求時,自定義反序列化過程就變得必要了。 為了修改序列化/反序列化過程中的行為,ObjectMapper 提供了範圍內的配置,我們可以設置這些配置。 因此,我們必須將此自定義 ObjectMapper 註冊到 Spring WebClient 中,以便在序列化和反序列化中使用。

3. 如何自定義對象映射器?

自定義 ObjectMapper 可以與 WebClient 在全局應用程序級別關聯,也可以與特定的請求關聯。

讓我們探索一個簡單的 API,它提供了一個用於客户訂單詳細信息的 GET 端點。 在本文中,我們將考慮訂單響應中的一些需要自定義反序列化的屬性,以滿足我們應用程序的特定功能。

讓我們看一下 OrderResponse 模型:

{
  "orderId": "a1b2c3d4-e5f6-4a5b-8c9d-0123456789ab",
  "address": [
    "123 Main St",
    "Apt 456",
    "Cityville"
  ],
  "orderNotes": [
    "Special request: Handle with care",
    "Gift wrapping required"
  ],
  "orderDateTime": "2024-01-20T12:34:56"
}

以下是上述客户響應的解封裝規則之一:

  • 如果客户訂單響應包含未知屬性,我們應使解封裝失敗。我們將設置 FAIL_ON_UNKNOWN_PROPERTIES 屬性為 true,在 ObjectMapper 中。
  • 由於 OrderDateTime 是一個 LocalDateTime 對象,我們還將向映射器添加 JavaTimeModule,用於解封裝目的。

4. 使用全局配置進行自定義反序列化

為了使用全局配置進行反序列化,我們需要註冊自定義的 ObjectMapper Bean:

@Bean
public ObjectMapper objectMapper() {
    return new ObjectMapper()
      .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true)
      .registerModule(new JavaTimeModule());
}

ObjectMapper Bean 在註冊後,將自動與 CodecCustomizer 相關聯,用於自定義應用程序與 WebClient 相關的編碼器和解碼器。

由此,它確保應用程序層級的任何請求或響應都將正確序列化和反序列化。

下面,我們定義一個帶有 GET 端點的控制器,該控制器調用外部服務以檢索訂單詳情:

@GetMapping(value = "v1/order/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
public Mono<OrderResponse> searchOrderV1(@PathVariable(value = "id") int id) {
    return externalServiceV1.findById(id)
      .bodyToMono(OrderResponse.class);
}

外部服務獲取訂單詳情將使用 `WebClient.Builder:

public ExternalServiceV1(WebClient.Builder webclientBuilder) {
    this.webclientBuilder = webclientBuilder;
}

public WebClient.ResponseSpec findById(int id) {
    return webclientBuilder.baseUrl("http://localhost:8090/")
      .build()
      .get()
      .uri("external/order/" + id)
      .retrieve();
}

Spring reactive 自動使用自定義的 ObjectMapper 解析獲取的 JSON 響應。

讓我們添加一個簡單的測試,使用 MockWebServerhttps://github.com/square/okhttp/tree/master/mockwebserver)模擬外部服務的響應,並添加額外的屬性,這將導致請求失敗。

@Test
void givenMockedExternalResponse_whenSearchByIdV1_thenOrderResponseShouldFailBecauseOfUnknownProperty() {

    mockExternalService.enqueue(new MockResponse().addHeader("Content-Type", "application/json; charset=utf-8")
      .setBody("""
        {
          "orderId": "a1b2c3d4-e5f6-4a5b-8c9d-0123456789ab",
          "orderDateTime": "2024-01-20T12:34:56",
          "address": [
            "123 Main St",
            "Apt 456",
            "Cityville"
          ],
          "orderNotes": [
            "Special request: Handle with care",
            "Gift wrapping required"
          ],
          "customerName": "John Doe",
          "totalAmount": 99.99,
          "paymentMethod": "Credit Card"
        }
        """)
      .setResponseCode(HttpStatus.OK.value()));

    webTestClient.get()
      .uri("v1/order/1")
      .exchange()
      .expectStatus()
      .is5xxServerError();
}

外部服務的響應包含額外的屬性(customerNametotalAmountpaymentMethod),導致測試失敗。

5. 使用 WebClient 交換策略自定義反序列化

在某些情況下,我們可能只想為特定請求配置一個 ObjectMapper,如果是這樣,則需要使用 ExchangeStrategies 將映射器註冊起來。

假設在上面的示例中接收到的日期格式與實際情況不同,並且包含時區偏移量。

我們將添加一個 CustomDeserializer,它將解析接收到的 OffsetDateTime 並將其轉換為模型中的 LocalDateTime (UTC 時間):

public class CustomDeserializer extends LocalDateTimeDeserializer {
    @Override
    public LocalDateTime deserialize(JsonParser jsonParser, DeserializationContext ctxt) throws IOException {
      try {
        return OffsetDateTime.parse(jsonParser.getText())
        .atZoneSameInstant(ZoneOffset.UTC)
        .toLocalDateTime();
      } catch (Exception e) {
          return super.deserialize(jsonParser, ctxt);
      }
    }
}

在新的 ExternalServiceV2 實現中,我們聲明一個新的 ObjectMapper,它與上述的 CustomDeserializer 聯動,並使用 ExchangeStrategies 註冊到一個新的 WebClient

public WebClient.ResponseSpec findById(int id) {

    ObjectMapper objectMapper = new ObjectMapper().registerModule(new SimpleModule().addDeserializer(LocalDateTime.class, new CustomDeserializer()));

    WebClient webClient = WebClient.builder()
      .baseUrl("http://localhost:8090/")
      .exchangeStrategies(ExchangeStrategies.builder()
      .codecs(clientDefaultCodecsConfigurer -> {
        clientDefaultCodecsConfigurer.defaultCodecs()
        .jackson2JsonEncoder(new Jackson2JsonEncoder(objectMapper, MediaType.APPLICATION_JSON));
        clientDefaultCodecsConfigurer.defaultCodecs()
        .jackson2JsonDecoder(new Jackson2JsonDecoder(objectMapper, MediaType.APPLICATION_JSON));
      })
      .build())
    .build();

    return webClient.get().uri("external/order/" + id).retrieve();
}

我們已將此 ObjectMapper 專門鏈接到一個特定的 API 請求,它不會應用於應用程序中的任何其他請求。接下來,讓我們添加一個 GET /v2 端點,它將使用上述的 findById 實現以及一個特定的 ObjectMapper 調用外部服務:

@GetMapping(value = "v2/order/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
public final Mono<OrderResponse> searchOrderV2(@PathVariable(value = "id") int id) {
    return externalServiceV2.findById(id)
      .bodyToMono(OrderResponse.class);
}

最後,我們將添加一個快速測試,其中我們傳遞一個模擬的 orderDateTime,並帶有偏移量,以驗證它是否使用 CustomDeserializer 將其轉換為 UTC:

@Test
void givenMockedExternalResponse_whenSearchByIdV2_thenOrderResponseShouldBeReceivedSuccessfully() {

    mockExternalService.enqueue(new MockResponse().addHeader("Content-Type", "application/json; charset=utf-8")
      .setBody("""
      {
        "orderId": "a1b2c3d4-e5f6-4a5b-8c9d-0123456789ab",
        "orderDateTime": "2024-01-20T14:34:56+01:00",
        "address": [
          "123 Main St",
          "Apt 456",
          "Cityville"
        ],
        "orderNotes": [
          "Special request: Handle with care",
          "Gift wrapping required"
        ]
      }
      """)
      .setResponseCode(HttpStatus.OK.value()));

    OrderResponse orderResponse = webTestClient.get()
      .uri("v2/order/1")
      .exchange()
      .expectStatus()
      .isOk()
      .expectBody(OrderResponse.class)
      .returnResult()
      .getResponseBody();
    assertEquals(UUID.fromString("a1b2c3d4-e5f6-4a5b-8c9d-0123456789ab"), orderResponse.getOrderId());
    assertEquals(LocalDateTime.of(2024, 1, 20, 13, 34, 56), orderResponse.getOrderDateTime());
    assertThat(orderResponse.getAddress()).hasSize(3);
    assertThat(orderResponse.getOrderNotes()).hasSize(2);
}

本測試調用 /v2 端點,該端點使用特定的 ObjectMapper,並結合 CustomDeserializer 解析來自外部服務的訂單詳情響應。

6. 結論

在本文中,我們探討了自定義反序列化的必要性以及不同的實現方法。我們首先研究了為整個應用程序以及特定請求註冊映射器的方法。我們還可以使用相同的配置來實施自定義序列化器。

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

發佈 評論

Some HTML is okay.