Java Top

Get started with Spring 5 and Spring Boot 2, through the Learn Spring course:

>> CHECK OUT THE COURSE

1. Overview

In this tutorial, we'll dive into the InstantSource interface introduced in Java 17, which provides a pluggable representation of the current instant and avoids references to time zones.

2. The InstantSource Interface

The first goal of this interface, as we can see in the original proposal and a related issue, is to create an abstraction to the time zone provided by java.time.Clock. It also eases the creation of stubs during testing for portions of code that retrieve instants.

It was added in Java 17 to provide a safe way to access the current instant, as we can see in the following example:

class AQuickTest {
    InstantSource source;
    ...
    Instant getInstant() {
        return source.instant();
    }
}

And then, we can simply get an instant:

var quickTest = new AQuickTest(InstantSource.system());
quickTest.getInstant();

Its implementations create objects which can be used anywhere to retrieve instants, and it provides an effective way to create stub implementations for testing purposes.

Let's take a deeper look at the benefits of using this interface.

3. Problems & Solutions

In order to better understand the InstantSource interface, let's dive into the problems it was created to address and the actual solutions it provides.

3.1. The Testing Issue

Testing code involving the retrieval of an Instant is usually a nightmare, and more so when the way to get that Instant is based on current data solutions, such as LocalDateTime.now().

In order for a test to provide a specific date, we usually create workarounds like creating an external date factory and providing a stubbed instance within the test.

Let's look at the following code as an example of a workaround for this issue.

The InstantExample class uses an InstantWrapper (or workaround) to recover an instant:

class InstantExample {
    InstantWrapper instantWrapper;
    Instant getCurrentInstantFromInstantWrapper() {
        return instantWrapper.instant();
    }
}

And our InstantWrapper workaround class itself looks like this:

class InstantWrapper {
    Clock clock;
    InstantWrapper() {
        this.clock = Clock.systemDefaultZone();
    }
    InstantWrapper(ZonedDateTime zonedDateTime) {
        this.clock = Clock.fixed(zonedDateTime.toInstant(), zonedDateTime.getZone());
    }
    Instant instant() {
        return clock.instant();
    }
}

Then, we can use it to provide a fixed instant for testing:

// given
LocalDateTime now = LocalDateTime.now();
InstantExample tested = new InstantExample(InstantWrapper.of(now), null);
Instant currentInstant = now.toInstant(ZoneOffset.UTC);
// when
Instant returnedInstant = tested.getCurrentInstantFromWrapper();
// then
assertEquals(currentInstant, returnedInstant);

3.2. Solution to the Testing Issue

In essence, the workaround we applied above is what InstantSource does. It provides an external factory of Instants that we can use wherever we need. Java 17 provides a default system-wide implementation (within the Clock class), and we can also provide our own:

class InstantExample {
    InstantSource instantSource;
    Instant getCurrentInstantFromInstantSource() {
        return instantSource.instant();
    }
}

The InstantSource is pluggable. That is, it can be injected using a dependency injection framework, or just passed as a constructor argument, into the object we're testing. Thus, we can easily create a stubbed InstantSource, provide it to the tested object, and have it return the instant we want for the test:

// given
LocalDateTime now = LocalDateTime.now();
InstantSource instantSource = InstantSource.fixed(now.toInstant(ZoneOffset.UTC));
InstantExample tested = new InstantExample(null, instantSource);
Instant currentInstant = instantSource.instant();
// when
Instant returnedInstant = tested.getCurrentInstantFromInstantSource();
// then
assertEquals(currentInstant, returnedInstant);

3.3. The Time Zone Issue

When we require an Instant, we have many different places to get it from, like Instant.now(), Clock.systemDefaultZone().instant() or even LocalDateTime.now.toInstant(zoneOffset). The problem is that depending on the flavor we choose, it could introduce time zone issues.

For instance, let's see what happens when we ask for an instant on the Clock class:

Clock.systemDefaultZone().instant();

This code will produce the following result:

2022-01-05T06:47:15.001890204Z

Let's ask the same instant but from a different source:

LocalDateTime.now().toInstant(ZoneOffset.UTC);

This produces the following output:

2022-01-05T07:47:15.001890204Z

We should have gotten the same instant, but in fact, there's a 60-minute difference between the two.

The worst part is that there might be two or more developers working on the same code using these two instant sources at different parts of the code. If that's the case, we have a problem.

We do not usually want to deal with time zones at this point. But, to create the instant, we need a source, and that source always comes with a time zone attached.

3.4. Solution to the Time Zone Issue

InstantSource abstracts us from selecting the source of the instants. That selection is already made for us. It could be that another programmer has set up a system-wide custom implementation or that we're using the one provided by Java 17, as we will see in the next section.

Just as the InstantExample shows, we got an InstantSource plugged in, and we don't need to know anything else. We can remove our InstantWrapper workaround and just use the plugged-in InstantSource instead.

Now that we've seen the benefits of using this interface let's take a look at what else it has to offer by going through its static and instance methods.

4. Factory Methods

The following factory methods can be used to create an InstantSource object:

  • system() – default system-wide implementation
  • tick(InstantSource, Duration) – returns an InstantSource truncated to the nearest representation of the given duration
  • fixed(Instant) – returns an InstantSource that always produces the same Instant
  • offset(InstantSource, Duration) – returns an InstantSource that provides Instants with the given offset

Let's see some basic usages of these methods.

4.1. system()

The current default implementation in Java 17 is the Clock.SystemInstantSource class.

Instant i = InstantSource.system().instant();

4.2. tick()

Based on the previous example:

Instant i = InstantSource.system().instant();
System.out.println(i);

After running this code we'll get the following output:

2022-01-05T07:44:44.861040341Z

But, if we apply a tick duration of 2 hours:

Instant i = InstantSource.tick(InstantSource.system(), Duration.ofHours(2)).instant();

Then, we'll get the result below:

2022-01-05T06:00:00Z

4.3. fixed()

This method is handy when we need to create a stubbed InstantSource for testing purposes:

LocalDateTime fixed = LocalDateTime.of(2022, 1, 1, 0, 0);
Instant i = InstantSource.fixed(fixed.toInstant(ZoneOffset.UTC)).instant();
System.out.println(i);

The above always returns the same instant:

2022-01-01T00:00:00Z

4.4. offset()

Based on the previous example, we'll apply an offset to the fixed InstantSource to see what it returns:

LocalDateTime fixed = LocalDateTime.of(2022, 1, 1, 0, 0);
InstantSource fixedSource = InstantSource.fixed(fixed.toInstant(ZoneOffset.UTC));
Instant i = InstantSource.offset(fixedSource, Duration.ofDays(5)).instant();
System.out.println(i);

After executing this code, we will get the following output:

2022-01-06T00:00:00Z

5. Instance Methods

The methods available to interact with an instance of InstantSource are:

  • instant() – returns the current Instant given by the InstantSource
  • millis() – returns the millisecond representation of the current Instant provided by the InstantSource
  • withZone(ZoneId) – receives a ZoneId and returns a clock based on the given InstantSource with the specified ZoneId

5.1. instant()

The most basic usage for this method is:

Instant i = InstantSource.system().instant();
System.out.println(i);

Running this code will show us the following output:

2022-01-05T08:29:17.641839778Z

5.2. millis()

In order to get the epoch from an InstantSource:

long m = InstantSource.system().millis();
System.out.println(m);

And, after running it we'll get the following:

1641371476655

5.3. withZone()

Let's get a Clock instance for a specific ZoneId:

Clock c = InstantSource.system().withZone(ZoneId.of("-4"));
System.out.println(c);

This will simply print the following:

SystemClock[-04:00]

6. Conclusion

In this article, we've gone through the InstantSource interface, enumerating the significant issues it was created to address and showing real-life examples of how we could take advantage of it in our daily work.

As usual, the code is available over on GitHub.

Java bottom

Get started with Spring 5 and Spring Boot 2, through the Learn Spring course:

>> CHECK OUT THE COURSE
Generic footer banner
guest
0 Comments
Inline Feedbacks
View all comments