為什麼不用其他方案?
| 方案 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|
| 統一 ObjectMapper(本文) | 全系統一致、自動時區轉換 | 需要初期設定 | 多平台整合系統 |
| 各處自行處理 | 彈性高 | 格式不一致、時區 bug 頻發 | 單一平台小專案 |
| 字串直接儲存 | 簡單 | 無法比較、排序困難 | 純展示用途 |
| Gson | 輕量 | 時區支援較弱、擴展性差 | 簡單 JSON 處理 |
前言:跨時區系統的日期處理
多通路系統需要處理各種日期格式:
| 平台 | 日期格式 | 範例 |
|---|---|---|
| 蝦皮 | Unix timestamp | 1710748800 |
| Yahoo | ISO 8601 | 2024-03-18T15:30:00+08:00 |
| Momo | 台灣時間字串 | 2024/03/18 15:30:00 |
| 資料庫 | UTC | 2024-03-18T07:30:00Z |
解決方案:統一 JSON 處理
ObjectMapper 設定
public class JsonConfig {
@Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
// 1. 允許序列化私有欄位
mapper.setVisibility(PropertyAccessor.FIELD, Visibility.ANY);
// 2. 日期格式設定
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
mapper.setDateFormat(new SimpleDateFormat(“yyyy-MM-dd’T’HH:mm:ss.SSS’Z’”));
// 3. 設定 UTC 時區
mapper.setTimeZone(TimeZone.getTimeZone(“UTC”));
// 4. 支援 Java 8 時間 API
mapper.registerModule(new JavaTimeModule());
// 5. 自定義序列化器
SimpleModule module = new SimpleModule();
module.addSerializer(ZonedDateTime.class, new ZonedDateTimeSerializer());
mapper.registerModule(module);
// 6. 忽略未知欄位(平台加新欄位不會報錯)
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
return mapper;
}
}
| 設定 | 效果 |
|---|---|
| FIELD visibility | 不需要 getter 也能序列化 |
| UTC 時區 | 系統內部統一用 UTC |
| JavaTimeModule | 支援 LocalDate、ZonedDateTime 等 |
| 忽略未知欄位 | API 加新欄位不會報錯 |
自定義日期序列化器
private static final DateTimeFormatter FORMATTER =
DateTimeFormatter.ofPattern(“yyyy-MM-dd’T’HH:mm:ss.SSS’Z’”);
@Override
public void serialize(
ZonedDateTime value,
JsonGenerator gen,
SerializerProvider provider) throws IOException {
// 無論輸入什麼時區,都轉成 UTC 輸出
ZonedDateTime utc = value.withZoneSameInstant(ZoneId.of(“UTC”));
gen.writeString(FORMATTER.format(utc));
}
}
效果展示
ZonedDateTime taiwanTime = ZonedDateTime.now(ZoneId.of(“Asia/Taipei”));
// 2024-03-18T15:30:00+08:00[Asia/Taipei]
// 輸出:自動轉成 UTC
String json = objectMapper.writeValueAsString(Map.of(“time”, taiwanTime));
// {“time”:”2024-03-18T07:30:00.000Z”}
JSON 工具類
public class JsonUtil {
private static ObjectMapper mapper;
@Autowired
public void setMapper(ObjectMapper mapper) {
JsonUtil.mapper = mapper;
}
/**
* 物件轉 JSON
*/
public static String toJson(Object obj) {
try {
return mapper.writeValueAsString(obj);
} catch (JsonProcessingException e) {
throw new JsonException(“序列化失敗”, e);
}
}
/**
* JSON 轉物件
*/
public static <T> T fromJson(String json, Class<T> clazz) {
try {
return mapper.readValue(json, clazz);
} catch (JsonProcessingException e) {
throw new JsonException(“反序列化失敗”, e);
}
}
/**
* JSON 轉泛型物件
*/
public static <T> T fromJson(String json, TypeReference<T> type) {
try {
return mapper.readValue(json, type);
} catch (JsonProcessingException e) {
throw new JsonException(“反序列化失敗”, e);
}
}
/**
* JSON 轉 List
*/
public static <T> List<T> fromJsonList(String json, Class<T> clazz) {
try {
JavaType type = mapper.getTypeFactory()
.constructCollectionType(List.class, clazz);
return mapper.readValue(json, type);
} catch (JsonProcessingException e) {
throw new JsonException(“反序列化失敗”, e);
}
}
/**
* JSON 轉 Map
*/
public static Map<String, Object> toMap(String json) {
return fromJson(json, new TypeReference<>() {});
}
}
平台日期轉換
/**
* Unix timestamp → ZonedDateTime(蝦皮)
*/
public static ZonedDateTime fromUnixTimestamp(long timestamp) {
return Instant.ofEpochSecond(timestamp)
.atZone(ZoneId.of(“UTC”));
}
/**
* ISO 8601 → ZonedDateTime(Yahoo)
*/
public static ZonedDateTime fromISO(String isoString) {
return ZonedDateTime.parse(isoString);
}
/**
* 台灣時間字串 → ZonedDateTime(Momo)
*/
public static ZonedDateTime fromTaiwanTime(String timeStr) {
DateTimeFormatter formatter =
DateTimeFormatter.ofPattern(“yyyy/MM/dd HH:mm:ss”);
LocalDateTime ldt = LocalDateTime.parse(timeStr, formatter);
return ldt.atZone(ZoneId.of(“Asia/Taipei”));
}
/**
* 轉成台灣時間顯示
*/
public static String toTaiwanDisplay(ZonedDateTime time) {
ZonedDateTime taiwanTime = time.withZoneSameInstant(
ZoneId.of(“Asia/Taipei”)
);
return taiwanTime.format(
DateTimeFormatter.ofPattern(“yyyy/MM/dd HH:mm:ss”)
);
}
}
使用範例
long shopeeTimestamp = 1710748800;
ZonedDateTime orderTime = DateConverter.fromUnixTimestamp(shopeeTimestamp);
// 存入資料庫(UTC)
order.setCreatedAt(orderTime);
orderRepository.save(order);
// 回傳 API(自動轉成 UTC JSON)
return JsonUtil.toJson(order);
// {“createdAt”:”2024-03-18T08:00:00.000Z”}
// 前端顯示(轉成台灣時間)
String display = DateConverter.toTaiwanDisplay(order.getCreatedAt());
// 2024/03/18 16:00:00
實戰踩坑
情境:前端顯示時間比實際晚 8 小時
原因:資料庫已經是 UTC,但讀取時又被當成本地時間再轉一次 UTC
解法:確保 JDBC 連線設定
serverTimezone=UTC,並且 Entity 用 ZonedDateTime 而非 Date
情境:訂單時間顯示成 1970 年
原因:蝦皮回傳秒級 timestamp,但程式用
Instant.ofEpochMilli() 處理解法:確認平台 API 文件的時間單位,秒用
ofEpochSecond,毫秒用 ofEpochMilli
情境:高併發時偶發日期解析錯誤或 NumberFormatException
原因:把 SimpleDateFormat 設成 static 共用
解法:改用
DateTimeFormatter(執行緒安全),或每次 new 新的 SimpleDateFormat
總結
| 設計 | 效果 |
|---|---|
| 統一 ObjectMapper | 全系統一致的 JSON 處理 |
| UTC 儲存 | 避免時區混亂 |
| 自定義序列化 | 控制輸出格式 |
| 平台轉換器 | 各平台格式統一處理 |
| 上一篇 | 系列目錄 | 下一篇 |
|---|---|---|
| PDF生成與Builder Pattern | 系列導讀 | OpenTracing分散式追蹤 |
這是「多通路電商 OMS 系統實戰」系列的第八篇。下一篇會介紹分散式追蹤。