Spring Boot環境における日時処理の設計と実装ガイド

Spring Bootアプリケーションを開発する際、システム内外での日付・時刻の扱いを適切に設計することはデータ整合性を保つために不可欠です。業務要件に応じて適切なクラスを選定し、シリアライズ設定や変換ロジックを標準化することで、予期せぬ時区ズレやフォーマットエラーを回避できます。

1. 利用可能な日時データ型と選定基準

  • java.util.Date:ミリ秒単位のレガシーAPI。JVM実行環境のデフォルトタイムゾーンに依存します。既存システムの保守や特定ORMライブラリの互換性維持に限定して使用するのが現実的です。
  • java.time.LocalDate:時区情報を含まない暦日(年月日)のみを保持します。ユーザーの生年月日、契約有効期限など、時刻情報が不要なフィールドに適用します。
  • java.time.LocalDateTime:日付と時刻を組み合わせますが時区は持ちません。アプリケーション内部の業務ログ、承認フローの記録、画面表示用の日時管理に最適です。
  • java.time.Instant:UTC基準の時間軸上的一点を表します。分散アーキテクチャ間の時刻同期、スロットル制御、経過時間計算など、数値計算を伴う処理に必須です。
  • Unix Timestamp:数値型(秒/ミリ秒)。Redis、Kafka、または外部マイクロサービスとの軽量ペイロード交換においてストレージ効率とパース速度が優れています。

2. データ型間の相互変換実装

Java 8以降のjava.timeパッケージはイミュータブル設計であり、変換時には中間的なInstantZonedDateTimeを経由するのが安全です。

// Unix秒数からUTC基準のInstantへ変換
long epochSec = 1753755417L;
Instant utcMoment = Instant.ofEpochSecond(epochSec);
// 必要に応じてミリ秒単位へ展開
long epochMillis = utcMoment.toEpochMilli();

// LocalDateTimeとタイムスタンプの相互変換(システムデフォルト時区適用)
ZoneId appZone = ZoneId.systemDefault();
LocalDateTime appTime = LocalDateTime.now();
long timestampFromLocal = appTime.atZone(appZone).toInstant().toEpochMilli();

LocalDateTime restoredTime = Instant.ofEpochMilli(timestampFromLocal)
        .atZone(appZone)
        .toLocalDateTime();
java.util.Date legacyDate = new java.util.Date();

// Date → LocalDateTime
LocalDateTime fromLegacy = legacyDate.toInstant()
        .atZone(ZoneId.systemDefault())
        .toLocalDateTime();

// LocalDateTime → Date
Date toLegacy = Date.from(
        appTime.atZone(ZoneId.systemDefault()).toInstant()
);

// LocalDateとの相互変換
LocalDate pureDate = LocalDate.now();
Date legacyFromDate = Date.from(
        pureDate.atStartOfDay(ZoneId.systemDefault()).toInstant()
);
LocalDate fromLegacyDate = legacyDate.toInstant()
        .atZone(ZoneId.systemDefault())
        .toLocalDate();

3. JSONシリアライズおよび入出力制御

API通信における文字列フォーマットは、グローバル設定またはフィールド単位のアノテーションで制御可能です。明示的な指定がなければJacksonはデフォルトのISO-8601形式や数値タイムスタンプを出力するため、環境差異によるトラブルを防ぐために固定化します。

グローバル設定(application.yml)

spring:
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: Asia/Shanghai
    serialization:
      write-dates-as-timestamps: false

フィールド単位のアノテーション

import com.fasterxml.jackson.annotation.JsonFormat;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import java.util.Date;

public class AuditDto {
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
    private Date executedAtLegacy;

    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime registeredAt;
}

手動フォーマット処理

ロギングやファイル出力など、JSON外で文字列へ変換する場合はDateTimeFormatterを使用します。スレッドセーフであるため、静的フィールドとしてキャッシュするのが一般的です。

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

private static final DateTimeFormatter CUSTOM_FMT = 
    DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");

String formattedString = LocalDateTime.now().format(CUSTOM_FMT);

4. アーキテクチャ上の推奨事項

  • 新旧APIの使い分け:新規モジュールではDateの使用を排除し、LocalDateTime(UI/ビジネスロジック)とInstant(インフラ/同期)を明確に役割分担させます。
  • APIペイロードの標準化:フロントエンドおよび外部連携はyyyy-MM-dd HH:mm:ssの文字列形式で統一します。数値タイムスタンプの秒/ミリ秒誤認識を防ぐ効果があります。
  • ストレージ設計:RDBMSでは業務日時にDATETIMEまたはTIMESTAMPを利用し、高頻度レンジクエリや分散ID生成用にはBIGINTを割り当てます。
  • 時区の明示的定義ZoneId.systemDefault()への過度な依存は避けます。コンテナ化環境ではホストOSとJVM設定が乖離しやすいため、ZoneId.of("Asia/Shanghai")など定数で固定するのが安全です。

5. 動作確認用実装セット

DTO定義、RESTコントローラー、およびcurlコマンドによる通信検証例です。

import com.fasterxml.jackson.annotation.JsonFormat;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import java.util.Date;

public class TransactionRecord {
    private String transactionId;

    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
    private Date executedAtLegacy;

    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime processedAt;

    public String getTransactionId() { return transactionId; }
    public void setTransactionId(String transactionId) { this.transactionId = transactionId; }
    public Date getExecutedAtLegacy() { return executedAtLegacy; }
    public void setExecutedAtLegacy(Date executedAtLegacy) { this.executedAtLegacy = executedAtLegacy; }
    public LocalDateTime getProcessedAt() { return processedAt; }
    public void setProcessedAt(LocalDateTime processedAt) { this.processedAt = processedAt; }
}
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/transactions")
public class TransactionController {

    @PostMapping
    public TransactionRecord saveRecord(@RequestBody TransactionRecord payload) {
        System.out.println("Legacy Date: " + payload.getExecutedAtLegacy());
        System.out.println("Modern DateTime: " + payload.getProcessedAt());
        return payload;
    }

    @GetMapping("/current")
    public TransactionRecord fetchCurrentState() {
        TransactionRecord record = new TransactionRecord();
        record.setExecutedAtLegacy(new Date());
        record.setProcessedAt(LocalDateTime.now());
        return record;
    }
}

動作検証用のcurlコマンドおよび期待されるレスポンス構造です。

# データ送信テスト
curl -X POST http://localhost:8080/api/transactions \
  -H "Content-Type: application/json" \
  -d '{"transactionId": "TX-9001", "executedAtLegacy": "2025-04-28 15:16:57", "processedAt": "2025-04-28 15:16:57"}'

# 現在時刻取得テスト
curl http://localhost:8080/api/transactions/current

上記設定が適用された状態では、以下のようなJSONレスポンスが返却されます。

{
  "transactionId": null,
  "executedAtLegacy": "2025-04-28 15:16:57",
  "processedAt": "2025-04-28 15:16:57"
}

タグ: Java Spring Boot Jackson java.time REST API

7月6日 01:15 投稿