1. 概述

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

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

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

Jackson库提供了一个使用ObjectMapperhttps://fasterxml.github.io/jackson-databind/javadoc/2.7/com/fasterxml/jackson/databind/ObjectMapper.html)的辅助工具,将Java对象序列化为JSON,并将JSON字符串反序列化为Java对象。ObjectMapper包含内置配置,可以通过反序列化特性启用或禁用。

当Jackson库提供的默认行为无法满足我们的特定需求时,就需要定制反序列化过程。为了在序列化和反序列化期间修改行为,ObjectMapper提供了我们可以设置的各种配置。因此,我们必须将这个自定义的ObjectMapper注册到Spring WebClient中,以便在序列化和反序列化时使用。

3. 如何定制ObjectMapper?

可以将自定义的ObjectMapper与WebClient在全球应用级别关联,也可以与特定请求关联。

让我们考虑一个提供获取客户订单详细信息的简单API。在本文中,我们将关注响应中的一些属性,这些属性需要我们为应用程序的特定功能进行自定义反序列化。

首先,看看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"
}

对于上述客户响应,一些反序列化的规则可能包括:

  • 如果客户订单响应包含未知属性,我们应该使反序列化失败。我们在ObjectMapper中设置FAIL_ON_UNKNOWN_PROPERTIES属性为true
  • 我们还会添加JavaTimeModule到映射器,因为OrderDateTime是一个LocalDateTime对象,用于反序列化目的。

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

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

@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响应。

让我们添加一个简单的测试,使用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 Exchange Strategies配置进行自定义反序列化

在某些情况下,我们可能只想为特定请求配置ObjectMapper,在这种情况下,我们需要将映射器与ExchangeStrategies注册。

假设上述示例中接收到的日期格式不同,包括偏移量。我们将添加一个CustomDeserializer,它将解析接收到的OffsetDateTime并将其转换为模型中的UTC LocalDateTime

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端点,它使用具有CustomDeserializer的特定ObjectMapper来解析从外部服务获取的订单详情响应。

6. 总结

在这篇文章中,我们探讨了自定义反序列化的必要性及其不同的实现方式。我们首先查看了为整个应用程序注册映射器,以及为特定请求注册映射器的方法。我们还可以使用相同的配置实现自定义序列化器。

如往常一样,示例代码可以在GitHub上找到这里