Custom JSON Deserialization Using Spring WebClient – 使用 Spring WebClient 自定义 JSON 反序列化

最后修改: 2024年 2月 2日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

In this article, we’ll explore the need for custom deserialization and how this can be implemented using Spring WebClient.

在本文中,我们将探讨自定义反序列化的需求以及如何使用 Spring WebClient 实现这一需求。

2. Why Do We Need Custom Deserialization?

2.为什么需要自定义反序列化?

Spring WebClient in the Spring WebFlux module handles serialization and deserialization through Encoder and Decoder components. The Encoder and Decoder exist as an interface representing the contracts to read and write content. By default, The spring-core module provides byte[], ByteBuffer, DataBuffer, Resource, and String encoder and decoder implementations.

Spring WebFlux 模块中的 Spring WebClient 通过 EncoderDecoder 组件处理序列化和反序列化。编码器和解码器作为接口存在,代表了读写内容的合约。默认情况下,spring-core 模块提供 byte[], ByteBuffer, DataBuffer, Resource, 和 String 编码器和解码器实现。

Jackson is a library that exposes helper utilities using ObjectMapper to serialize Java objects into JSON and deserialize JSON strings into Java objects. ObjectMapper contains built-in configurations that can be turned on/off using the deserialization feature.

Jackson 是一个库,它使用 ObjectMapper 公开了辅助实用程序,用于将 Java 对象序列化为 JSON 并将 JSON 字符串反序列化为 Java 对象。ObjectMapper 包含内置配置,可使用 反序列化功能打开/关闭这些配置。

Customizing the deserialization process becomes necessary when the default behavior offered by the Jackson Library proves inadequate for our specific requirements. To modify the behavior during serialization/deserialization, ObjectMapper provides a range of configurations that we can set. Consequently, we must register this custom ObjectMapper with Spring WebClient for use in serialization and deserialization.

当 Jackson Library 提供的默认行为无法满足我们的特定需求时,就有必要自定义反序列化流程。为了修改序列化/反序列化过程中的行为,ObjectMapper 提供了一系列配置供我们设置。因此,我们必须向 Spring WebClient 注册此自定义 ObjectMapper 以用于序列化和反序列化。

3. How to Customize Object Mappers?

3.如何自定义对象映射器?

A custom ObjectMapper can be linked with WebClient at the global application level or can be associated with a specific request.

自定义 ObjectMapper 可以在全局应用程序级别与 WebClient 关联,也可以与特定请求关联。

Let’s explore a simple API that provides a GET endpoint for customer order details. In this article, we’ll consider some of the attributes in the order response that require custom deserialization for our application’s specific functionality.

让我们来探索一个简单的 API,它提供了一个用于获取客户订单详细信息的 GET 端点。在本文中,我们将考虑订单响应中的一些属性,这些属性需要针对我们应用程序的特定功能进行自定义反序列化。

Let’s have a look at the OrderResponse model:

让我们来看看 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"
}

Some of the deserialization rules for the above customer response would be:

上述客户回复的一些反序列化规则是

  • If the customer order response contains unknown properties, we should make the deserialization fail. We’ll set the FAIL_ON_UNKNOWN_PROPERTIES property to true in ObjectMapper.
  • We’ll also add the JavaTimeModule to the mapper for deserialization purposes since OrderDateTime is a LocalDateTime object.

4. Custom Deserialization Using Global Config

4.使用全局配置自定义反序列化

To deserialize using Global Config, we need to register the custom ObjectMapper bean:

要使用全局配置进行反序列化,我们需要注册自定义 ObjectMapper Bean:

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

This ObjectMapper bean, upon registration, will be automatically linked with CodecCustomizer to customize the encoder and decoder associated with the application WebClient. Consequently, it ensures that any request or response at the application level is serialized and deserialized accordingly.

ObjectMapper Bean 在注册后将自动与 CodecCustomizer 关联,以自定义与应用程序 WebClient 关联的编码器和解码器。因此,它可确保应用程序级别的任何请求或响应都能相应地序列化和反序列化。

Let’s define a controller with a GET endpoint that invokes an external service to retrieve order details:

让我们定义一个带有 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);
}

The external service that retrieves the order details will use the WebClient.Builder:

检索订单详细信息的外部服务将使用 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 automatically uses the custom ObjectMapper to parse the retrieved JSON response.

Spring reactive 会自动使用自定义 ObjectMapper 来解析检索到的 JSON 响应。

Let’s add a simple test that uses  MockWebServer to mock the external service response with additional attributes, and this should cause the request to fail:

让我们添加一个简单的测试,使用 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();
}

The response from the external service contains additional attributes (customerNametotalAmount, paymentMethod) which causes the test to fail.

外部服务的响应包含附加属性(客户名称总金额付款方式),导致测试失败。

5. Custom Deserialization Using WebClient Exchange Strategies Config

5.使用 WebClient 交换策略配置自定义反序列化

In certain situations, we might want to configure an ObjectMapper only for specific requests, and in that case, we need to register the mapper with ExchangeStrategies.

在某些情况下,我们可能希望仅针对特定请求配置 ObjectMapper,在这种情况下,我们需要使用 ExchangeStrategies 注册映射器。

Let’s assume that the date format received is different in the above example and includes an offset.

假设在上述示例中接收到的日期格式不同,并且包含偏移量。

We’ll add a CustomDeserializer, which will parse the received OffsetDateTime and convert it to the model LocalDateTime in UTC:

我们将添加一个 自定义解串器,它将解析接收到的 OffsetDateTime 并将其转换为模型 LocalDateTime in 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);
      }
    }
}

In a new implementation of ExternalServiceV2, let’s declare a new ObjectMapper that links with the above CustomDeserializer and register it with a new WebClient using ExchangeStrategies:

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

We have linked this ObjectMapper exclusively with a specific API request, and it will not apply to any other requests within the application. Next, let’s add a GET /v2 endpoint that will invoke an external service using the above findById implementation along with a specific ObjectMapper:

我们已将此 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);
}

Finally, we’ll add a quick test where we pass a mocked orderDateTime with an offset and validate if it uses the CustomDeserializer to convert it to UTC:

最后,我们将添加一个快速测试,传递一个带有偏移量的模拟 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);
}

This test invokes the /v2 endpoint, which uses a specific ObjectMapper with CustomDeserializer to parse the order details response received from an external service.

该测试调用 /v2 端点,它使用特定的 ObjectMapperCustomDeserializer 来解析从外部服务接收到的订单详细信息响应。

6. Conclusion

6.结论

In this article, we explored the need for custom deserialization and different ways to implement it. We first looked at registering a mapper for the entire application and also for specific requests. We can also use the same configurations to implement a custom serializer.

在这篇文章中,我们探讨了自定义反序列化的需求和不同的实现方法。我们首先探讨了为整个应用程序和特定请求注册映射器的问题。我们还可以使用相同的配置来实现自定义序列化器。

As always, the source code for the examples is available over on GitHub.

与往常一样,这些示例的源代码可在 GitHub 上获取。