---
title: InstantSourceでJavaのシステム時刻の作成を抽象化しテスタビリティを上げる
summary: この記事は、JDK17のInstantSourceを使ってシステム時刻を抽象化し、DIやテストで簡単に制御する方法を解説します。
tags: ["Java", "Testing", "JUnit", "PostgreSQL", "Spring Boot"]
categories: ["Programming", "Java", "java", "time"]
date: 2025-12-19T06:54:14Z
updated: 2025-12-19T11:22:54Z
---

Javaでシステム時刻(現在時刻)を取得する際に`Instant.now()`や`LocalDateTime.now()`などを使うことが一般的です。しかし、これらのメソッドはOSのシステムクロックに依存しているため、テスト時にシステム時刻を制御することが難しくなります。
システム時刻の作成を抽象化するためのクラスとしてJDKには

* `java.time.Clock` - JDK 8で追加
* `java.time.InstantSource` - JDK 17で追加

が用意されています。`Clock`と`InstantSource`との違いは前者はタイムゾーンを保持していることです。`InstantSource`は`java.time.Instant`生成のみを扱います。
また、`Clock`はabstractクラスですが、`InstantSource`はinterfaceです。

`Clock`を使う場合は、次のように日付・時刻を取得します。

```java
Clock clock = Clock.systemUTC();
Clock clock = Clock.systemDefaultZone();

Instant now = clock.instant();
OffsetDateTime now = OffsetDateTime.now(clock);
LocalDateTime now = LocalDateTime.now(clock);
LocalDate now = LocalDate.now(clock);
```

`InstantSource`を使う場合は、次のように時刻を取得します。

```java
InstantSource instantSource = InstantSource.system();

Instant now = instantSource.instant();
```

システム時刻（`Instant`）とユーザーのローカライゼーション（`ZoneId`）は本来別々の関心毎ですが、Clockではこれらが結合しています。
シンプルにシステム時刻だけを取得するインタフェースがあるべきだ、ということでJDK 17で`InstantSource`が追加されました。
より詳細な経緯は[こちら](https://mail.openjdk.org/pipermail/core-libs-dev/2021-May/077213.html)から確認できます。

以下では`InstantSource`を使った例を示しますが、`Clock`は`InstantSource`インタフェースを実装しており、`Clock`のインスタンスは`InstantSource`としても利用できます。
おそらく、日本国内でのみ利用されるシステムのように、タイムゾーンが固定されるケースでは`Clock`を使った方が`LocalDate`の生成など便利な場合が多いかもしれません。
なお、`InstantSource`から`Clock`への変換は次のように行えます。

```java
Clock clock = instantSource.withZone(ZoneId.systemDefault());
```

さて、実際のアプリケーションにおいては`InstantSource`はDependency Injection(DI)コンテナなどを使って注入することが一般的です。例えば、Spring Bootを使っている場合は次のようにBean定義を行います:


```java
import java.time.InstantSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration(proxyBeanMethods = false)
class AppConfig {

	@Bean
	InstantSource instantSource() {
		return InstantSource.system();
	}

}
```

`InstantSource`はFunctional Interfaceなので、次のようにラムダ式で実装することも可能です。

```java
	@Bean
	InstantSource instantSource() {
		return Instant::now;
	}
```

コード中で`Instant`を作成したい場合は、`InstantSource`をinjectして利用します。

```java
@Service
public class MessageService {
	private final InstantSource instantSource;

	public MessageService(InstantSource instantSource) {
		this.instantSource = instantSource;
	}

	public Message createMessage(String content) {
		Instant now = instantSource.instant();
		return new Message(content, now);
	}
}
```

`InstantSource`はインタフェースなので、テスト時に差し替えるのが楽です。
テストコードの例を示します。ここではMockitoを使って`InstantSource`をモック化し、特定の時刻を返却するようにしています。

```java
import java.time.Instant;
import java.time.InstantSource;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.context.junit.jupiter.SpringExtension;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;

@ExtendWith(SpringExtension.class)
@Import(MessageService.class)
class MessageServiceTest {

	@Autowired
	MessageService messageService;

	@MockitoBean
	InstantSource instantSource;

	@Test
	void createMessage() {
		given(instantSource.instant()).willReturn(Instant.parse("2026-01-01T00:00:00.00Z"));
		Message message = messageService.createMessage("Hello, World!");
		assertThat(message.toString()).isEqualTo("Message[content=Hello, World!, timestamp=2026-01-01T00:00:00Z]");
	}

}
```

`LocalDate`を作成したい場合は、次のようにします。

```java
		ZoneId zoneId = ZoneId.systemDefault(); // or ZoneId.of("Asia/Tokyo");
		LocalDate now = instantSource.instant().atZone(zoneId).toLocalDate();
```

あるいは、injectionのタイミングでタイムゾーンを設定する方が良いかもしれません。

```java
@Service
public class MessageService {
	private final Clock clock;

	public MessageService(InstantSource instantSource) {
		this.clock = instantSource.withZone(ZoneId.systemDefault());
	}

	public Message createMessage(String content) {
		LocalDate now = LocalDate.now(this.clock);
		return new Message(content, now);
	}
}
```

ユーザー毎によって`ZoneId`を変えたいという場合には`org.springframework.format.datetime.standard.DateTimeContextHolder`でスレッドローカルに`ZoneId`を保持する方法などもあります。

エンタープライズ開発でよく見られるのはシステム時刻をデータベースから取得するケースです。システムテストなどで、特定の時間のテストを行いたい場合などに有効です。
データベースからシステム時刻を取得する例を示します。ここではPostgreSQLを前提とします。

次のように、特定のシステム時刻を設定するテーブルがあるとします。

```sql
CREATE TABLE IF NOT EXISTS system_date
(
    date_time TIMESTAMP WITH TIME ZONE
);
```

このテーブルにデータが存在する場合はその日時を、存在しない場合はデータベースの現在時刻をシステム時刻として取得する例を示します。

```java
import java.time.InstantSource;
import java.time.OffsetDateTime;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.simple.JdbcClient;

@Configuration(proxyBeanMethods = false)
class AppConfig {

	@Bean
	InstantSource jdbcInstantSource(JdbcClient jdbcClient) {
		return () -> jdbcClient.sql("SELECT COALESCE((SELECT date_time FROM system_date), NOW())")
			.query(OffsetDateTime.class)
			.single()
			.toInstant();
	}

}
```

あるいは、データベース上に、現在時刻からのオフセット(分)を保持するテーブルがある場合は、次のように実装できます。

```sql
CREATE TABLE system_date
(
    offset_minutes INT NOT NULL
);
```

このテーブルにデータが存在する場合はその値を分に、存在しない場合は0をシステム時刻に追加する例を示します。

```java
import java.time.InstantSource;
import java.time.OffsetDateTime;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.simple.JdbcClient;

@Configuration(proxyBeanMethods = false)
class AppConfig {

	@Bean
	InstantSource jdbcInstantSource(JdbcClient jdbcClient) {
		return () -> jdbcClient.sql("""
				SELECT
				    NOW() + MAKE_INTERVAL(
				        mins => COALESCE(MAX(offset_minutes), 0)
				    )
				FROM
				    system_date
				""").query(OffsetDateTime.class).single().toInstant();
	}

}
```

例えば、1時間先の時刻でテストしたい場合は、次のようなレコードを挿入すれば良いです。

```sql
INSERT INTO system_date(offset_minutes) VALUES (60);
```

テストが終わり、このレコードを削除すると、通常の現在時刻を返すようになります。

これらを使うことで、アプリケーションを起動し直すことなく、データベースの内容を変更するだけでシステム時刻を変更できるようになります。

---

`InstantSource`を使うことで、システム時刻の取得を抽象化し、テスト時に時刻を自由に制御できるようになります。
`Instant.now()`や`LocalDate.now()`を直接使用している場合は、`InstantSource`を経由するように変更して、よりテスタブルなコードを書くことをお勧めします。
