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パッケージはイミュータブル設計であり、変換時には中間的なInstantやZonedDateTimeを経由するのが安全です。
// 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"
}