使用 Spring WebClient 自訂 JSON 反序列化
1. 概述
在本文中,我們將探討自訂反序列化的需求以及如何使用 Spring WebClient 來實現這一點。
2. 為什麼我們需要自訂反序列化?
Spring WebFlux 模組中的 Spring WebClient 透過Encoder
和Decoder
元件處理序列化和反序列化。編碼器和Decoder
作為表示讀取和寫入內容的合約的介面而存在。預設情況下, spring-core 模組提供byte
[]、 ByteBuffer
、 DataBuffer
、 Resource
和String
編碼器和解碼器實作。
Jackson 是一個庫,它公開使用ObjectMapper
幫助程序實用程序,將 Java 物件序列化為 JSON 並將 JSON 字串反序列化為 Java 物件。 ObjectMapper
包含可以使用反序列化功能開啟/關閉的內建配置。
當 Jackson 函式庫提供的預設行為不足以滿足我們的特定要求時,客製化反序列化過程就變得很有必要。為了修改序列化/反序列化期間的行為,ObjectMapper 提供了一系列我們可以設定的配置。因此,我們必須向 Spring WebClient 註冊這個自訂ObjectMapper
以用於序列化和反序列化。
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"
}
上述客戶回應的一些反序列化規則如下:
- 如果客戶訂單回應包含未知屬性,我們應該使反序列化失敗。我們將在
ObjectMapper
中將FAIL_ON_UNKNOWN_PROPERTIES
屬性設為 true - 我們也將
JavaTimeModule
新增至映射器以用於反序列化,因為OrderDateTime
是LocalDateTime
對象
在這裡,我們定義了ObjectMapper,
它使用這些反序列化功能:
@Bean
public ObjectMapper objectMapper() {
return new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true)
.registerModule(new JavaTimeModule());
}
4. 使用全域配置自訂反序列化
要使用全域配置進行反序列化,我們需要向CodecCustomizer
註冊自訂ObjectMapper
以自訂與WebClient
關聯的編碼器和解碼器:
@Bean
public CodecCustomizer codecCustomizer(ObjectMapper customObjectMapper) {
return configurer -> {
MimeType mimeType = MimeType.valueOf(MediaType.APPLICATION_JSON_VALUE);
CodecConfigurer.CustomCodecs customCodecs = configurer.customCodecs();
customCodecs.register(new Jackson2JsonDecoder(customObjectMapper, mimeType));
customCodecs.register(new Jackson2JsonEncoder(customObjectMapper, mimeType));
};
}
這個 bean,即CodecCustomizer
,有效地為應用程式的上下文配置ObjectMapper
。因此,它確保應用程式層級的任何請求或回應都相應地序列化和反序列化。
讓我們定義一個帶有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](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/reactive/function/client/WebClient.Builder.html)
:
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 React 會自動使用自訂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 Exchange 策略配置自訂反序列化
在某些情況下,我們可能只想為特定請求配置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的新實作中,我們宣告一個與上述CustomDeserializer
連結的新ObjectMapper
,並使用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
來解析從外部服務接收的訂單詳細資訊回應。
六,結論
在本文中,我們探討了自訂反序列化的必要性以及實現它的不同方法。我們首先考慮為整個應用程式以及特定請求註冊映射器。我們還可以使用相同的配置來實現自訂序列化器。
與往常一樣,範例的原始程式碼可在 GitHub 上取得。