DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Last call! Secure your stack and shape the future! Help dev teams across the globe navigate their software supply chain security challenges.

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Releasing software shouldn't be stressful or risky. Learn how to leverage progressive delivery techniques to ensure safer deployments.

Avoid machine learning mistakes and boost model performance! Discover key ML patterns, anti-patterns, data strategies, and more.

Related

  • Be Punctual! Avoiding Kotlin’s lateinit In Spring Boot Testing
  • Spring Boot: How To Use Java Persistence Query Language (JPQL)
  • Distributed Tracing System (Spring Cloud Sleuth + OpenZipkin)
  • Java, Spring Boot, and MongoDB: Performance Analysis and Improvements

Trending

  • AI’s Role in Everyday Development
  • Fixing Common Oracle Database Problems
  • Scaling Mobile App Performance: How We Cut Screen Load Time From 8s to 2s
  • Analyzing “java.lang.OutOfMemoryError: Failed to create a thread” Error
  1. DZone
  2. Coding
  3. Frameworks
  4. Spring WebClient and Java Date-Time Fields

Spring WebClient and Java Date-Time Fields

Learn more about using Spring's WebClient for Java date and time fields.

By 
Biju Kunjummen user avatar
Biju Kunjummen
·
Jan. 23, 20 · Tutorial
Likes (6)
Comment
Save
Tweet
Share
22.6K Views

Join the DZone community and get the full member experience.

Join For Free

What time is it? Java time!

WebClient is the Spring Framework's reactive client for making service-to-service calls. WebClient has become a go-to utility for me; however, I unexpectedly encountered an issue recently in the way it handles Java 8 time fields that tripped me up. This post will go into the details of the date and time fields in Java.

You may also like: Java 8 Date and Time

Happy Path

First, the happy path. When using a WebClient, Spring Boot advises a WebClient.Builder to be injected into a class instead of the WebClient itself and a WebClient.Builder is already auto-configured and available for injection.

Consider a fictitious "City" domain and a client to create a "City". "City" has a simple structure — note that the creationDate is a Java 8 "Instant" type:

Java




xxxxxxxxxx
1


 
1
import java.time.Instant
2
 
          
3
data class City(
4
    val id: Long,
5
    val name: String,
6
    val country: String,
7
    val pop: Long,
8
    val creationDate: Instant = Instant.now()
9
)



The client to create an instance of this type looks like this:

Java




x
25


 
1
class CitiesClient(
2
    private val webClientBuilder: WebClient.Builder,
3
    private val citiesBaseUrl: String
4
) {
5
    fun createCity(city: City): Mono<City> {
6
        val uri: URI = UriComponentsBuilder
7
            .fromUriString(citiesBaseUrl)
8
            .path("/cities")
9
            .build()
10
            .encode()
11
            .toUri()
12
 
          
13
        val webClient: WebClient = this.webClientBuilder.build()
14
 
          
15
        return webClient.post()
16
            .uri(uri)
17
            .contentType(MediaType.APPLICATION_JSON)
18
            .accept(MediaType.APPLICATION_JSON)
19
            .bodyValue(city)
20
            .exchange()
21
            .flatMap { clientResponse ->
22
                clientResponse.bodyToMono(City::class.java)
23
            }
24
    }
25
}



See how the intent is expressed in a fluent way. The URI and the headers are first being set; the request body is then put in place and the response is unmarshalled back to a "City" response type.

All is well and good. Now, what does a test look like?

I am using the excellent Wiremock to bring up a dummy remote service and using this  CitiesClient to send the request, along these lines:

Java




xxxxxxxxxx
1
67


 
1
@SpringBootTest 
2
@AutoConfigureJson
3
class WebClientConfigurationTest {
4
 
          
5
    @Autowired
6
    private lateinit var webClientBuilder: WebClient.Builder
7
 
          
8
    @Autowired
9
    private lateinit var objectMapper: ObjectMapper
10
 
          
11
    @Test
12
    fun testAPost() {
13
        val dateAsString = "1985-02-01T10:10:10Z"
14
 
          
15
        val city = City(
16
            id = 1L, name = "some city",
17
            country = "some country",
18
            pop = 1000L,
19
            creationDate = Instant.parse(dateAsString)
20
        )
21
        WIREMOCK_SERVER.stubFor(
22
            post(urlMatching("/cities"))
23
                .withHeader("Accept", equalTo("application/json"))
24
                .withHeader("Content-Type", equalTo("application/json"))
25
                .willReturn(
26
                    aResponse()
27
                        .withHeader("Content-Type", "application/json")
28
                        .withStatus(HttpStatus.CREATED.value())
29
                        .withBody(objectMapper.writeValueAsString(city))
30
                )
31
        )
32
 
          
33
        val citiesClient = CitiesClient(webClientBuilder, "http://localhost:${WIREMOCK_SERVER.port()}")
34
 
          
35
        val citiesMono: Mono<City> = citiesClient.createCity(city)
36
 
          
37
        StepVerifier
38
            .create(citiesMono)
39
            .expectNext(city)
40
            .expectComplete()
41
            .verify()
42
 
          
43
 
          
44
        //Ensure that date field is in ISO-8601 format..
45
        WIREMOCK_SERVER.verify(
46
            postRequestedFor(urlPathMatching("/cities"))
47
                .withRequestBody(matchingJsonPath("$.creationDate", equalTo(dateAsString)))
48
        )
49
    }
50
 
          
51
    companion object {
52
        private val WIREMOCK_SERVER =
53
            WireMockServer(WireMockConfiguration.wireMockConfig().dynamicPort().notifier(ConsoleNotifier(true)))
54
 
          
55
        @BeforeAll
56
        @JvmStatic
57
        fun beforeAll() {
58
            WIREMOCK_SERVER.start()
59
        }
60
 
          
61
        @AfterAll
62
        @JvmStatic
63
        fun afterAll() {
64
            WIREMOCK_SERVER.stop()
65
        }
66
    }
67
}



In the highlighted lines, I want to make sure that the remote service receives the date in ISO-8601 format as "1985-02-01T10:10:10Z". In this instance, everything works cleanly and the test passes.

Not-So-Happy Path

Consider now a case where I have customized the WebClient.Builder in some form. Here's an example. Say I am using a registry service and I want to look up a remote service via this registry and then make a call. Then, the WebClient has to be customized to add a @LoadBalanced annotation on it. More details can be found here.

So say I customized WebClient.Builder this way:

Java




xxxxxxxxxx
1
15


 
1
@Configuration
2
class WebClientConfiguration {
3
 
          
4
    @Bean
5
    fun webClientBuilder(): WebClient.Builder {
6
        return WebClient.builder().filter { req, next ->
7
            LOGGER.error("Custom filter invoked..")
8
            next.exchange(req)
9
        }
10
    }
11
 
          
12
    companion object {
13
        val LOGGER = loggerFor<WebClientConfiguration>()
14
    }
15
}



It looks straightforward. However, now, the previous test fails. Specifically, the date format of the  creationDate over the wire is not ISO-8601 anymore. The raw request looks like this:

Java




xxxxxxxxxx
1


 
1
{
2
    "id": 1,
3
    "name": "some city",
4
    "country": "some country",
5
    "pop": 1000,
6
    "creationDate": 476100610.000000000
7
}



And here's what it looks like for a working request:

Java




xxxxxxxxxx
1


 
1
{
2
    "id": 1,
3
    "name": "some city",
4
    "country": "some country",
5
    "pop": 1000,
6
    "creationDate": "1985-02-01T10:10:10Z"
7
}



See how the date format is different?

Problem

The underlying reason for this issue is simple: Spring Boot adds a bunch of configuration on  WebClient.Builder that is lost when I have explicitly created the bean myself. Specifically, in this instance, there is a Jackson ObjectMapper created under the covers, which, by default, writes dates as timestamps. More details can be found here.

Solution

Okay, so how do we retrieve customizations made in Spring Boot? I have essentially replicated the behavior of auto-configuration in Spring called WebClientAutoConfiguration, and it looks like this:

Java




xxxxxxxxxx
1
23


 
1
@Configuration
2
class WebClientConfiguration {
3
 
          
4
    @Bean
5
    fun webClientBuilder(customizerProvider: ObjectProvider<WebClientCustomizer>): WebClient.Builder {
6
        val webClientBuilder: WebClient.Builder = WebClient
7
            .builder()
8
            .filter { req, next ->
9
                LOGGER.error("Custom filter invoked..")
10
                next.exchange(req)
11
            }
12
 
          
13
        customizerProvider.orderedStream()
14
            .forEach { customizer -> customizer.customize(webClientBuilder) }
15
 
          
16
        return webClientBuilder;
17
    }
18
 
          
19
    companion object {
20
        val LOGGER = loggerFor<WebClientConfiguration>()
21
    }
22
}
23
 
          



There is a likely a better approach than just replicating this behavior, but this approach works for me.

The posted content now looks like this:

Java




xxxxxxxxxx
1


 
1
{
2
    "id": 1,
3
    "name": "some city",
4
    "country": "some country",
5
    "pop": 1000,
6
    "creationDate": "1985-02-01T10:10:10Z"
7
}



... with the date back in the right format.

Conclusion

Spring Boot's auto-configurations for  WebClient provides an opinionated set of defaults. If for any reason the  WebClient and it's builder need to be configured explicitly, then be wary of some of the customizations that Spring Boot adds and replicate it for the customized bean. In my case, the Jackson customization for Java 8 dates was missing in my custom WebClient.Builder and had to be explicitly accounted for.

A sample test and customization is available here.

Thanks for reading!

Further Reading

Java 8 Date and Time

Spring Boot WebClient and Unit Testing

Reactive Programming in Java: Using the WebClient Class

Spring Framework Java (programming language) Spring Boot

Published at DZone with permission of Biju Kunjummen, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Be Punctual! Avoiding Kotlin’s lateinit In Spring Boot Testing
  • Spring Boot: How To Use Java Persistence Query Language (JPQL)
  • Distributed Tracing System (Spring Cloud Sleuth + OpenZipkin)
  • Java, Spring Boot, and MongoDB: Performance Analysis and Improvements

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!