Merge pull request #15702 from balasr21/master
BAEL-7272: Added code for custom deserialization using Spring WebClient
This commit is contained in:
commit
463972ab84
|
@ -64,6 +64,11 @@
|
|||
<groupId>org.springframework.session</groupId>
|
||||
<artifactId>spring-session-data-redis</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.squareup.okhttp3</groupId>
|
||||
<artifactId>mockwebserver</artifactId>
|
||||
<version>4.12.0</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<dependencyManagement>
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
package com.baeldung.custom.deserialization;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class Application {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(Application.class, args);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package com.baeldung.custom.deserialization.config;
|
||||
|
||||
import org.springframework.boot.web.codec.CodecCustomizer;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.codec.CodecConfigurer;
|
||||
import org.springframework.http.codec.json.Jackson2JsonDecoder;
|
||||
import org.springframework.http.codec.json.Jackson2JsonEncoder;
|
||||
import org.springframework.util.MimeType;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
@Configuration
|
||||
public class CodecCustomizerConfig {
|
||||
|
||||
@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));
|
||||
};
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
package com.baeldung.custom.deserialization.config;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package com.baeldung.custom.deserialization.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
|
||||
@Configuration
|
||||
public class CustomObjectMapper {
|
||||
@Bean
|
||||
public ObjectMapper objectMapper() {
|
||||
return new ObjectMapper()
|
||||
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true)
|
||||
.registerModule(new JavaTimeModule());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
package com.baeldung.custom.deserialization.controller;
|
||||
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import com.baeldung.custom.deserialization.model.OrderResponse;
|
||||
import com.baeldung.custom.deserialization.service.ExternalServiceV1;
|
||||
import com.baeldung.custom.deserialization.service.ExternalServiceV2;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@RestController
|
||||
public class OrderController {
|
||||
|
||||
private final ExternalServiceV1 externalServiceV1;
|
||||
private final ExternalServiceV2 externalServiceV2;
|
||||
|
||||
public OrderController(ExternalServiceV1 externalServiceV1, ExternalServiceV2 externalServiceV2) {
|
||||
this.externalServiceV1 = externalServiceV1;
|
||||
this.externalServiceV2 = externalServiceV2;
|
||||
}
|
||||
|
||||
@GetMapping(value = "v1/order/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public final Mono<OrderResponse> searchOrderV1(@PathVariable(value = "id") int id) {
|
||||
return externalServiceV1.findById(id)
|
||||
.bodyToMono(OrderResponse.class);
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package com.baeldung.custom.deserialization.model;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class OrderResponse {
|
||||
|
||||
private UUID orderId;
|
||||
|
||||
private LocalDateTime orderDateTime;
|
||||
|
||||
private List<String> address;
|
||||
|
||||
private List<String> orderNotes;
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package com.baeldung.custom.deserialization.service;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
@Component
|
||||
public class ExternalServiceV1 {
|
||||
|
||||
private final WebClient.Builder webclientBuilder;
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
package com.baeldung.custom.deserialization.service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.codec.json.Jackson2JsonDecoder;
|
||||
import org.springframework.http.codec.json.Jackson2JsonEncoder;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.client.ExchangeStrategies;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
import com.baeldung.custom.deserialization.config.CustomDeserializer;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule;
|
||||
|
||||
@Component
|
||||
public class ExternalServiceV2 {
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,135 @@
|
|||
package com.baeldung.custom.deserialization;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
|
||||
import com.baeldung.custom.deserialization.model.OrderResponse;
|
||||
|
||||
import okhttp3.mockwebserver.MockResponse;
|
||||
import okhttp3.mockwebserver.MockWebServer;
|
||||
|
||||
@SpringBootTest(classes = Application.class, webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
|
||||
@TestPropertySource(properties = "server.port=8091")
|
||||
@AutoConfigureWebTestClient(timeout = "100000")
|
||||
class OrderControllerIntegrationTest {
|
||||
|
||||
@Autowired
|
||||
private WebTestClient webTestClient;
|
||||
|
||||
private static MockWebServer mockExternalService;
|
||||
|
||||
@BeforeAll
|
||||
static void setup() throws IOException {
|
||||
mockExternalService = new MockWebServer();
|
||||
mockExternalService.start(8090);
|
||||
}
|
||||
|
||||
@Test
|
||||
void givenMockedExternalResponse_whenSearchByIdV1_thenOrderResponseShouldFailBecauseOfUnknownProperty() {
|
||||
|
||||
mockExternalService.enqueue(new MockResponse().addHeader("Content-Type", "application/json; charset=utf-8")
|
||||
.setBody("{\n" + " \"orderId\": \"a1b2c3d4-e5f6-4a5b-8c9d-0123456789ab\",\n"
|
||||
+ " \"orderDateTime\": \"2024-01-20T12:34:56\",\n"
|
||||
+ " \"address\": [\"123 Main St\", \"Apt 456\", \"Cityville\"],\n"
|
||||
+ " \"orderNotes\": [\"Special request: Handle with care\", \"Gift wrapping required\"],\n"
|
||||
+ " \"customerName\": \"John Doe\",\n" + " \"totalAmount\": 99.99,\n"
|
||||
+ " \"paymentMethod\": \"Credit Card\"\n" + " }")
|
||||
.setResponseCode(HttpStatus.OK.value()));
|
||||
|
||||
webTestClient.get()
|
||||
.uri("v1/order/1")
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.is5xxServerError();
|
||||
}
|
||||
|
||||
@Test
|
||||
void givenMockedExternalResponse_whenSearchByIdV1_thenOrderResponseShouldBeReceivedSuccessfully() {
|
||||
|
||||
mockExternalService.enqueue(new MockResponse().addHeader("Content-Type", "application/json; charset=utf-8")
|
||||
.setBody("{\n" + " \"orderId\": \"a1b2c3d4-e5f6-4a5b-8c9d-0123456789ab\",\n"
|
||||
+ " \"orderDateTime\": \"2024-01-20T12:34:56\",\n"
|
||||
+ " \"address\": [\"123 Main St\", \"Apt 456\", \"Cityville\"],\n"
|
||||
+ " \"orderNotes\": [\"Special request: Handle with care\", \"Gift wrapping required\"]\n"
|
||||
+ " }")
|
||||
.setResponseCode(HttpStatus.OK.value()));
|
||||
|
||||
OrderResponse orderResponse = webTestClient.get()
|
||||
.uri("v1/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, 12, 34, 56), orderResponse.getOrderDateTime());
|
||||
assertThat(orderResponse.getAddress()).hasSize(3);
|
||||
assertThat(orderResponse.getOrderNotes()).hasSize(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void givenMockedExternalResponse_whenSearchByIdV2_thenOrderResponseShouldFailBecauseOfUnknownProperty() {
|
||||
|
||||
mockExternalService.enqueue(new MockResponse().addHeader("Content-Type", "application/json; charset=utf-8")
|
||||
.setBody("{\n" + " \"orderId\": \"a1b2c3d4-e5f6-4a5b-8c9d-0123456789ab\",\n"
|
||||
+ " \"orderDateTime\": \"2024-01-20T12:34:56\",\n"
|
||||
+ " \"address\": [\"123 Main St\", \"Apt 456\", \"Cityville\"],\n"
|
||||
+ " \"orderNotes\": [\"Special request: Handle with care\", \"Gift wrapping required\"],\n"
|
||||
+ " \"customerName\": \"John Doe\",\n"
|
||||
+ " \"totalAmount\": 99.99,\n"
|
||||
+ " \"paymentMethod\": \"Credit Card\"\n"
|
||||
+ " }")
|
||||
.setResponseCode(HttpStatus.OK.value()));
|
||||
|
||||
webTestClient.get()
|
||||
.uri("v2/order/1")
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.is5xxServerError();
|
||||
}
|
||||
|
||||
@Test
|
||||
void givenMockedExternalResponse_whenSearchByIdV2_thenOrderResponseShouldBeReceivedSuccessfully() {
|
||||
|
||||
mockExternalService.enqueue(new MockResponse().addHeader("Content-Type", "application/json; charset=utf-8")
|
||||
.setBody("{\n" + " \"orderId\": \"a1b2c3d4-e5f6-4a5b-8c9d-0123456789ab\",\n"
|
||||
+ " \"orderDateTime\": \"2024-01-20T14:34:56+01:00\",\n"
|
||||
+ " \"address\": [\"123 Main St\", \"Apt 456\", \"Cityville\"],\n"
|
||||
+ " \"orderNotes\": [\"Special request: Handle with care\", \"Gift wrapping required\"]\n" + " }")
|
||||
.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);
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void tearDown() throws IOException {
|
||||
mockExternalService.shutdown();
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue