WebClient是Spring Framework的反应式客户端,用于进行服务到服务的调用。
WebClient已成为我的实用工具,但是最近我意外地遇到了一个问题,即它处理Java 8时间字段的方式使我绊倒了,本文对此进行了详细介绍。
快乐之路
首先是幸福的道路。 使用WebClient时, Spring Boot建议将“ WebClient.Builder”注入到类中,而不是“ WebClient”本身,并且已经自动配置了WebClient.Builder并可以注入。
考虑一个虚拟的“城市”域和一个创建“城市”的客户。 “城市”具有简单的结构,请注意creationDate是Java8“即时”类型:
import java.time.Instant data class City( val id: Long, val name: String, val country: String, val pop: Long, val creationDate: Instant = Instant.now() )
用于创建此类型实例的客户端如下所示:
CitiesClient( class CitiesClient( private val webClientBuilder: WebClient.Builder, private val citiesBaseUrl: String ) { fun createCity(city: City): Mono<City> { val uri: URI = UriComponentsBuilder .fromUriString(citiesBaseUrl) .path( "/cities" ) .build() .encode() .toUri() val webClient: WebClient = this .webClientBuilder.build() return webClient.post() .uri(uri) .contentType(MediaType.APPLICATION_JSON) .accept(MediaType.APPLICATION_JSON) .bodyValue(city) .exchange() .flatMap { clientResponse -> clientResponse.bodyToMono(City:: class .java) } } }
了解如何以一种流畅的方式表达意图。 首先设置uri和标头,然后放置请求主体,然后将响应解组回“ City”响应类型。
一切都很好。 现在测试看起来如何。
我正在使用出色的Wiremock来启动虚拟远程服务,并使用此CitiesClient发送请求,方法如下:
@SpringBootTest @AutoConfigureJson WebClientConfigurationTest { class WebClientConfigurationTest { @Autowired private lateinit var webClientBuilder: WebClient.Builder @Autowired private lateinit var objectMapper: ObjectMapper @Test fun testAPost() { val dateAsString = "1985-02-01T10:10:10Z" val city = City( id = 1L, name = "some city" , country = "some country" , pop = 1000L, creationDate = Instant.parse(dateAsString) ) WIREMOCK_SERVER.stubFor( post(urlMatching( "/cities" )) .withHeader( "Accept" , equalTo( "application/json" )) .withHeader( "Content-Type" , equalTo( "application/json" )) .willReturn( aResponse() .withHeader( "Content-Type" , "application/json" ) .withStatus(HttpStatus.CREATED.value()) .withBody(objectMapper.writeValueAsString(city)) ) ) val citiesClient = CitiesClient(webClientBuilder, " http://localhost: ${WIREMOCK_SERVER.port()}" ) val citiesMono: Mono<City> = citiesClient.createCity(city) StepVerifier .create(citiesMono) .expectNext(city) .expectComplete() .verify() //Ensure that date field is in ISO-8601 format.. WIREMOCK_SERVER.verify( postRequestedFor(urlPathMatching( "/cities" )) .withRequestBody(matchingJsonPath( "$.creationDate" , equalTo(dateAsString))) ) } companion object { private val WIREMOCK_SERVER = WireMockServer(WireMockConfiguration.wireMockConfig().dynamicPort().notifier(ConsoleNotifier( true ))) @BeforeAll @JvmStatic fun beforeAll() { WIREMOCK_SERVER.start() } @AfterAll @JvmStatic fun afterAll() { WIREMOCK_SERVER.stop() } } }
在突出显示的行中,我要确保远程服务以ISO-8601格式接收日期为“ 1985-02-01T10:10:10Z”。 在这种情况下,一切正常进行,测试通过了。
不太开心的路
现在考虑以某种形式自定义WebClient.Builder的情况。 一个例子是说我正在使用注册表服务,并且我想通过此注册表查找远程服务,然后打电话,然后必须自定义WebClient以在其上添加“ @LoadBalanced”注释- 这里有一些详细信息
可以这么说,我以这种方式自定义了WebClient.Builder:
@Configuration WebClientConfiguration { class WebClientConfiguration { @Bean fun webClientBuilder(): WebClient.Builder { return WebClient.builder().filter { req, next -> LOGGER.error( "Custom filter invoked.." ) next.exchange(req) } } companion object { val LOGGER = loggerFor<WebClientConfiguration>() } }
它看起来很简单,但是现在以前的测试失败了。 具体来说,网上的creationDate的日期格式不再是ISO-8601,原始请求如下所示:
{ "id" : 1 , "name" : "some city" , "country" : "some country" , "pop" : 1000 , "creationDate" : 476100610.000000000 }
与工作要求:
{ "id" : 1 , "name" : "some city" , "country" : "some country" , "pop" : 1000 , "creationDate" : "1985-02-01T10:10:10Z" }
查看日期格式有何不同。
问题
这个问题的根本原因很简单,Spring Boot在WebClient.Builder上添加了一堆配置,当我自己明确创建bean时,这些配置会丢失。 特别是在这种情况下,在后台创建了一个Jackson ObjectMapper,默认情况下将日期写为时间戳– 此处有一些详细信息。
解
好的,那么我们如何取回Spring Boot进行的自定义。 我实质上已经在Spring中复制了称为“ WebClientAutoConfiguration”的自动配置的行为,它看起来像这样:
@Configuration WebClientConfiguration { class WebClientConfiguration { @Bean fun webClientBuilder(customizerProvider: ObjectProvider<WebClientCustomizer>): WebClient.Builder { val webClientBuilder: WebClient.Builder = WebClient .builder() .filter { req, next -> LOGGER.error( "Custom filter invoked.." ) next.exchange(req) } customizerProvider.orderedStream() .forEach { customizer -> customizer.customize(webClientBuilder) } return webClientBuilder; } companion object { val LOGGER = loggerFor<WebClientConfiguration>() } }
除了复制这种行为,可能还有更好的方法,但是这种方法对我有用。
现在发布的内容如下所示:
{ "id" : 1 , "name" : "some city" , "country" : "some country" , "pop" : 1000 , "creationDate" : "1985-02-01T10:10:10Z" }
日期以正确的格式显示。
结论
Spring Boot对WebClient的自动配置提供了一套明确的默认值。 如果出于任何原因需要显式配置WebClient及其构建器,请警惕Spring Boot添加的一些自定义项并将其复制为自定义bean。 在我的案例中,我的自定义“ WebClient.Builder”中缺少针对Java 8日期的Jackson定制,因此必须明确说明。
此处提供示例测试和自定义
翻译自: https://www.javacodegeeks.com/2020/01/spring-webclient-and-java-date-time-fields.html