1. 概述
本文將探討自定義反序列化的必要性,以及如何使用 Spring WebClient 實現它。
2. 為什麼我們需要自定義反序列化?
Spring WebClient 在 Spring WebFlux 模塊中通過 Encoder 和 Decoder 組件處理序列化和反序列化。 Encoder 和 Decoder 作為接口存在,代表了讀取和寫入內容之間的合同。 默認情況下, spring-core 模塊提供 byte[]、ByteBuffer、DataBuffer、Resource 和 String 編碼器和解碼器實現。
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 響應。
讓我們添加一個簡單的測試,使用 MockWebServer(https://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();
}外部服務的響應包含額外的屬性(customerName、totalAmount、paymentMethod),導致測試失敗。
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. 結論
在本文中,我們探討了自定義反序列化的必要性以及不同的實現方法。我們首先研究了為整個應用程序以及特定請求註冊映射器的方法。我們還可以使用相同的配置來實施自定義序列化器。