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 using getTemporal() which may return either a java.time.Duration or a java.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?

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