Introduction
This article shares my experience with code refactoring using Java Time.
Globally, the goal is to make the code more concise by moving the complexity to
Java Time classes java.time.*
. This article will mainly focus on
java.time.Instant
and java.time.Duration
and will share some examples in several
popular Java frameworks.
After reading this article, you will understand:
- Some advantages of using Java Time
- Examples in Completable Future
- Examples in Jackson
- Examples in Akka
- Examples in Elasticsearch
Now, let’s get started!
Motivation
Why using Java Time?
Value + Time Unit. When using types like java.time.Duration
, it represents
not only the value but the time unit associated with this value as well. By
encapsulating these two notions together, it makes the code safer.
Immutable. All the date-time objects are immutable in Java Time. So you don’t have to worry about the value being modified by others.
Transformation and manipulation. When transforming a date object from one type to another type, it might be error-prone or verbose. Using Java Time makes things simpler because the framework provides many methods for transformation and handles the complexity for you. Also, when trying to manipulate a date by adding duration, or when trying the compare two dates, it’s easier as well.
Timezone support. Timezone support is also a valuable point. Types like
ZonedDateTime
or Instant
contain timezone information. It gives you
support if your application needs it.
There are many other advantages, but we are no going to dig deeper into this
subject. Now, if we focus on the application side: how to use Java Time in
different situations? In the following sections, we are going to talk about
a list of brief introductions over some popular
Java frameworks: Java Concurrency (java.util.concurrency.*
), Jackson, Akka,
and Elasticsearch.
Completable Future
Java Concurrency classes use two fields to control the timeout: the value of the
timeout and its unit. The value of the timeout is usually a long
and the unit
of the timeout is usually an item in enum java.util.concurrent.TimeUnit
:
NANOSECONDS, MICROSECONDS, MILLISECONDS, SECONDS, MINUTES, HOURS, DAYS. For
example, the get-with-timeout method in CompletableFuture
:
public T get(long timeout, TimeUnit unit) { ... }
The problem with using a long as a timeout in the code is that we don’t know the unit about it. Is it in milliseconds, seconds, minutes, hours, …? Unless adding the unit in the variable name or add a comment, there is no other way to know the unit. The actual code looks like this:
var cf = CompletableFuture.completedFuture("hello");
var timeoutInSeconds = 5;
var message = cf.get(timeoutInSeconds, TimeUnit.SECONDS);
This makes the code verbose, requires value conversion face to unit changes, and
requires all the variables having this unit. A better alternitive is to use
Duration
everywhere and only convert to “value + unit” in the caller.
var cf = CompletableFuture.completedFuture("hello");
var timeout = Duration.ofSeconds(5);
var message = cf.get(timeout.toSeconds(), TimeUnit.SECONDS);
To preserve precision, you should also use a smaller unit, such as using milliseconds instead of seconds.
Jackson
Jackson is a famous framework to handle serialization between Java and JSON. This is particularly true for RESTful API and non-relational databases, such as Jersey and MongoDB. Here I want to discuss two cases: using timestamp format in JSON or using ISO-8601 string format in JSON.
Case 1: using timestamp. Storing date using timestamp means storing an integer (long) in the JSON document. It is either an epoch timestamp in second or an epoch in millisecond. This is a simple solution. If you already have an existing data model, you may want to preserve this because there is no migration required for existing documents. The inconvenience of this solution is that the date itself is not human-readable. Also, we cannot store the timezone information in the same field. When choosing this approach, you don’t need to change anything about Jackson. To use Java Time in this case, you can create a computed field in your Java model, which converts the epoch timestamp into a Java Time object.
{ "value" : 1601510400 }
class ClassA {
@JsonProperty("value")
private final long value;
ClassA(@JsonProperty("value") long value) {
this.value = value;
}
@JsonIgnore
public Instant instant() {
return Instant.ofEpochSecond(value);
}
}
Case 2: using ISO-8601 string. Storing date using ISO-8601 (wikipedia) means that you need to register an additional Jackson module to have this capability and configure Jackson to serialize and deserialize Java Time objects.
{ "value" : "2020-10-01T00:00:00Z" }
class ClassB {
@JsonProperty("value")
private final Instant value;
ClassB(@JsonProperty("value") Instant value) {
this.value = value;
}
}
To have this capacity, you need to declare dependency as follows if you are using Maven:
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
Then you need to register the JavaTimeModule
to your object mapper. For
serialization, you need to ask Jackson to write dates as ISO-8601 string instead
of timestamp by disabling the serialization feature WRITE_DATES_AS_TIMESTAMPS.
var objectMapper = new ObjectMapper();
/*
* Registry Java Time Module to serialize Java Time objects.
* see https://github.com/FasterXML/jackson-modules-java8.
*/
objectMapper.registerModule(new JavaTimeModule());
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
As for deserialization, there is nothing to do. If there is anything unclear,
you can visit the GitHub project of jackson-modules-java8
to find more detail:
https://github.com/FasterXML/jackson-modules-java8.
Akka (Typesafe Config)
Akka uses Typesafe config to configure the actor system. Typesafe config (https://github.com/lightbend/config) is a configuration library for JVM languages using HOCON files. If you never use it before, you can try it as follows:
<dependency>
<groupId>com.typesafe</groupId>
<artifactId>config</artifactId>
<version>1.4.1</version>
</dependency>
In this section, let’s compare two examples, without and with Java Time.
Case 1: without Java Time. Without Java Time, our time-related properties will be stored as an integer (long) and then will be associated with a unit when using it. This is a bad idea because you need to find a way to remember the unit of the time properties and ensure everything is consistent across the codebase.
timeout: 1000 # ms
Config config = ConfigFactory.parseString("timeout: 1000 # ms");
// We don't know the unit of this value, we trust the variable
// name and associate a unit when using this variable
long timeoutInMillis = config.getLong("timeout");
Case 2: with Java Time.
Using Java Time in Typesafe config library is a good idea because it
encapsulates the value and the unit when constructing the Duration
object.
We can also convert it to a specific value under a given time unit (millisecond,
second, minute, hour, …). Typesafe config provides a method for retrieving a
duration, it’s Config#getDuration(String)
:
timeout: 1000ms
Config config = ConfigFactory.parseString("timeout: 1000ms");
Duration timeout = config.getDuration("timeout");
The configuration files of Typesafe config is written in a Humain-Optimized Config Object Notation (HOCON) format, which has complete support for duration and period.
For duration format, the following strings are supported. They are
case-sensitive and must be written in lowercase. You can use them for your
time properties and retrieve it using getDuration
:
ns
,nano
,nanos
,nanosecond
,nanoseconds
us
,micro
,micros
,microsecond
,microseconds
ms
,milli
,millis
,millisecond
,milliseconds
s
,second
,seconds
m
,minute
,minutes
h
,hour
,hours
d
,day
,days
For period format, you can use getPeriod()
. The following strings are
supported. They are case-sensitive and must be written in lowercase. You can use
them for your date-based properties:
d
,day
,days
w
,week
,weeks
m
,mo
,month
,months
(note that if you are usinggetTemporal()
which may return either ajava.time.Duration
or ajava.time.Period
you will want to use mo rather than m to prevent your unit being parsed as minutes)y
,year
,years
For more information, please check the official documentation of HOCON.
Elasticsearch
Elasticsearch has its time utility class called TimeValue
. It is used when you
retrieve a time value from the Elasticsearch settings:
// settings = { "timeout" : "5m" }
TimeValue timeout = settings.getAsTime("timeout", TimeValue.ZERO);
You can use the following syntax to convert a time value into a Java Time
Duration
if you know the precision of the value is lower than milliseconds,
such as seconds, minutes, or hours:
Duration duration = Duration.ofMillis(timeValue.millis());
And use the following syntax to convert a Java Time Duration
back to a
TimeValue
:
TimeValue timeValue = TimeValue.timeValueMillis(duration.toMillis());
Going Further
How to go further from here?
- To learn more about Java Time, visit the Java Specification Request (JSR) 310 - Date and Time API https://jcp.org/en/jsr/detail?id=310
- To learn more about Jackson’s support about Java Time (JSR-310), visit GitHub
project
jackson-modules-java8
, a multi-module umbrella project for Jackson modules needed to support Java 8 features. https://github.com/FasterXML/jackson-modules-java8 - To learn more about HOCON (Human-Optimized Config Object Notation), read the specification here https://github.com/lightbend/config/blob/master/HOCON.md
- To learn more about Elasticsearch, read the Elasticsearch official documentation as “Elasticsearch Reference” https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html
- To learn how to use
java.time.Clock
to control date-time objects in Java Time, visit my blog “Controlling Time with Java Clock” https://mincong.io/2020/05/24/java-clock/.
If you want to find the source code of this blog, you can find them here on
GitHub projects:
mincong-h/java-examples
(concurrency, jackson,
config) and mincong-h/learning-elasticsearch
(link).
Conclusion
In this article, we talked about how to use Java Time in some Java frameworks:
Java Concurrency package java.util.concurrent.*
, JSON serialization
framework Jackson, actor model Akka and Elasticsearch.
Interested to know more? You can subscribe to the feed of my blog, follow me
on Twitter or
GitHub. Hope you enjoy this article, see you the next time!
References
- Baeldung, “Jackson Date”, Baeldung, 2020.
https://www.baeldung.com/jackson-serialize-dates - Jackson Authors, “Jackson Modules Java 8”, GitHub, 2020.
https://github.com/FasterXML/jackson-modules-java8 - Wikipedia, “ISO 8601”, Wikipedia, 2020.
https://en.wikipedia.org/wiki/ISO_8601