Spring Boot内の現在日時を変更する

# 前提
プロジェクトをテストする中でどうしてもアプリ日時を変更したいときがあると思います。
アプリを起動するコンテナの日時を変更しても実現はできますが、HTTPS通信をする際にエラーが発生することがありました。そのためClockクラスを利用して日時変更をする方法を試したのでメモです。

コードサンプルはこちらです。

https://github.com/y5347M/clock-config-sample

# 環境
- macOS: 12.6
- IntelliJ IDEA: 2022.2.3 (Community Edition)
- Gradle: 7.5
- Java: temurin 17.0.3
- Spring Boot: 2.7.10-SNAPSHOT

# 手順

## プロジェクトの作成

#### spring initializrでプロジェクト作成
[https://start.spring.io/](https://start.spring.io/)でプロジェクトの雛形を作成します。
以下の画像のような構成にしています。
<img width="800" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2433961/3452ce5e-d040-3c3d-dc55-8a4575a56f47.png">

GENERATEボタンを押してダウンロードしてきます。それをIDEツールで開きます。

#### コントローラー作成

`http://localhost:8080/api/datetime`にアクセスすることでレスポンスが返却されるAPIを作成しました。
`LocalDateTime.now()`の代わりに今回作成する`CustomLocalDateTime.customLocalDateTime()`等を使用しています。

```java:DatetimeController.java
@RestController
@RequestMapping("/api/datetime")
public class DatetimeController {

    @Data
    @Builder
    public static class Response {
        private LocalDateTime localDateTime;
        private LocalDate localDate;
        private LocalTime localTime;
    }

    @GetMapping
    public Response getCurrentDatetime() {
        return Response.builder()
                .localDateTime(CustomLocalDateTime.customLocalDateTime())
                .localDate(CustomLocalDateTime.customLocalDate())
                .localTime(CustomLocalDateTime.customLocalTime())
                .build();
    }
}
```

#### ClockConfigクラス作成
Configクラスを作成します。
システム起動時にappilication.ymlから値を読み取り、時間指定されていればその時間を反映したClock、そうでなければシステム日時を反映したClockを返却します。
あらかじめZoneは`Asia/Tokyo`を指定しておきます。

```java:ClockConfig.java
@Configuration
@Slf4j
public class ClockConfig {

    @Value("${time-travel.specified-time}")
    private String specifiedTime;

    @Bean
    public Clock clock() {
        if(ObjectUtils.isEmpty(specifiedTime)) {
            log.info("Using default clock");
            return Clock.system(ZoneId.of("Asia/Tokyo"));
        } else {
            log.info("Using custom clock: {}", specifiedTime);
            return Clock.fixed(Instant.parse(specifiedTime), ZoneId.of("Asia/Tokyo"));
        }
    }

}
```

applicaiton.ymlには以下の設定を追加します。
```yml:application.yml
time-travel:
  specified-time: 
```

#### CustomLocalDateTimeクラス作成
beanとして用意したClockを各Component上でDIしてもいいのですが、管理が煩雑になります。そのため時間を管理するUtilクラスを作成して利用します。

staticであるUtilクラスにconfigをセットするため、`setClockConfig`メソッドを用意しておきます。

```java:CustomLocalDateTime.java
public class CustomLocalDateTime {

    private static ClockConfig clockConfig;

    public static void setClockConfig(ClockConfig clockConfig) {
        CustomLocalDateTime.clockConfig = clockConfig;
    }

    private CustomLocalDateTime() {}

    public static LocalDateTime customLocalDateTime() {
        return LocalDateTime.now(clockConfig.clock());
    }

    public static LocalDate customLocalDate() {
        return LocalDate.now(clockConfig.clock());
    }

    public static LocalTime customLocalTime() {
        return LocalTime.now(clockConfig.clock());
    }
}
```

#### StaticContextInitializerクラス作成
staticであるUtilクラスにconfigをセットするため、アプリケーション起動時に動作するようStaticContextInitializerクラスを準備します。参考にしたのは以下のサイトです。

https://better-coding.com/java-spring-boot-how-to-autowire-bean-to-a-static-field-of-a-class/

```java:StaticContextInitializer.java
@Component
public class StaticContextInitializer {

    @Autowired
    private ClockConfig clockConfig;

    @PostConstruct
    public void init() {
        CustomLocalDateTime.setClockConfig(clockConfig);
    }
}
```


## 実行

#### 日時を指定しない場合
applicaiton.ymlには以下の設定を追加します。
```yml:application.yml
time-travel:
  specified-time: 
```

アプリケーションを起動します。
ログにはデフォルトのClockを使用するという内容が表示されます。
```console:log
2023-03-05 11:34:07.851  INFO 9299 --- [           main] c.e.c.config.ClockConfig                 : Using default clock
```

`http://localhost:8080/api/datetime`にアクセスします。
レスポンスには現在日時が表示されます。
```console:レスポンス
{"localDateTime":"2023-03-05T11:35:16.674985","localDate":"2023-03-05","localTime":"11:35:16.675263"}
```

#### 日時を指定する場合
applicaiton.ymlには以下の設定を追加します。
```yml:application.yml
time-travel:
  specified-time: 2025-01-01T12:00:00.000Z
```

アプリケーションを起動します。
ログには指定された日時のClockを使用するという内容が表示されます。
```console:log
2023-03-05 11:38:07.522  INFO 9373 --- [           main] c.e.c.config.ClockConfig                 : Using custom clock: 2025-01-01T12:00:00.000Z
```

`http://localhost:8080/api/datetime`にアクセスします。
レスポンスには指定された日時が表示されます。
注意が必要なのは、指定した時間はUTCで表示されるのはJSTとなる点です。こちらは各プロジェクト内で統一してください。変更する場合はClockConfigクラスのゾーンを変更してください。
```console:レスポンス
{"localDateTime":"2025-01-01T21:00:00","localDate":"2025-01-01","localTime":"21:00:00"}
```

# さいごに
アプリ内の日時を変更してテストすることが多いと思います。一つの例として参考にしていただけると幸いです。
他の方法があればぜひご共有ください！