1. 时间毫秒数转日期字符串的需求背景
在Java开发中,我们经常需要处理时间戳与日期字符串之间的相互转换。特别是在以下场景中:
- 数据库存储的时间字段通常以长整型毫秒数保存
- 前端展示需要格式化的日期字符串(如"2023-07-15 14:30:00")
- 日志记录时需要人类可读的时间格式
- 不同系统间的时间数据交换
我刚接手一个电商项目时,就遇到过这样的问题:订单系统中的创建时间在数据库存的是Long型时间戳,但运营人员需要导出Excel报表时要求显示标准日期格式。最初团队是手动拼接字符串,结果出现了时区错乱、格式不统一的问题。
2. 核心转换方案对比
2.1 SimpleDateFormat方案
这是最传统的实现方式:
java复制public static String longToDateString(long timestamp) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return sdf.format(new Date(timestamp));
}
优点:
- 简单直观,学习成本低
- 支持自定义日期格式模式
注意事项:
- SimpleDateFormat不是线程安全的,每次调用应该创建新实例
- 时区问题需要显式设置:
java复制sdf.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai")); - 性能较差(实测百万次转换比DateTimeFormatter慢3倍)
2.2 DateTimeFormatter方案(Java 8+)
Java 8引入的新日期API更推荐使用:
java复制public static String longToDateString(long timestamp) {
DateTimeFormatter formatter = DateTimeFormatter
.ofPattern("yyyy-MM-dd HH:mm:ss")
.withZone(ZoneId.of("Asia/Shanghai"));
return Instant.ofEpochMilli(timestamp)
.atZone(ZoneId.systemDefault())
.format(formatter);
}
优势对比:
| 特性 | SimpleDateFormat | DateTimeFormatter |
|---|---|---|
| 线程安全 | 否 | 是 |
| 性能 | 慢 | 快(约3倍) |
| 时区处理 | 需要手动设置 | 内置支持 |
| API设计 | 老旧 | 现代流畅 |
2.3 第三方库方案
对于复杂场景,可以考虑:
-
Joda-Time(历史项目常见):
java复制new DateTime(timestamp).toString("yyyy-MM-dd HH:mm:ss"); -
Apache Commons Lang:
java复制DateFormatUtils.format(timestamp, "yyyy-MM-dd HH:mm:ss");
提示:新项目建议直接使用Java 8的DateTime API,避免引入额外依赖
3. 性能优化实践
3.1 缓存格式化对象
对于SimpleDateFormat,可以借助ThreadLocal优化:
java复制private static final ThreadLocal<SimpleDateFormat> dateFormatCache =
ThreadLocal.withInitial(() -> {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
sdf.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai"));
return sdf;
});
public static String longToDateString(long timestamp) {
return dateFormatCache.get().format(new Date(timestamp));
}
3.2 批量处理优化
当需要转换大量时间戳时:
java复制// 使用DateTimeFormatter的预编译特性
private static final DateTimeFormatter formatter = DateTimeFormatter
.ofPattern("yyyy-MM-dd HH:mm:ss")
.withZone(ZoneId.of("Asia/Shanghai"));
public static List<String> batchConvert(List<Long> timestamps) {
return timestamps.stream()
.map(ts -> Instant.ofEpochMilli(ts).atZone(ZoneId.systemDefault()))
.map(zdt -> zdt.format(formatter))
.collect(Collectors.toList());
}
4. 时区问题深度解析
这是最容易踩坑的地方,常见问题场景:
-
服务器默认时区与业务需求不符
- 解决方案:始终显式指定时区
java复制// 推荐使用IANA时区标识(如Asia/Shanghai) ZoneId.of("Asia/Shanghai") -
夏令时导致的重复或缺失时间
- 测试用例:转换2020-03-29 02:30:00(欧洲夏令时切换点)
- 处理方案:使用ZonedDateTime代替LocalDateTime
-
数据库存储与展示时区不一致
- 最佳实践:存储UTC时间,展示时转换
java复制Instant.ofEpochMilli(timestamp) .atZone(ZoneId.of("UTC")) .withZoneSameInstant(ZoneId.of("Asia/Shanghai"))
5. 格式自定义扩展
5.1 常用格式模式
| 模式字符 | 含义 | 示例 |
|---|---|---|
| yyyy | 四位年份 | 2023 |
| MM | 两位月份(补零) | 07 |
| dd | 两位日期(补零) | 05 |
| HH | 24小时制(补零) | 14 |
| mm | 分钟(补零) | 09 |
| ss | 秒数(补零) | 05 |
| SSS | 毫秒 | 789 |
5.2 复合格式示例
java复制// 带毫秒的格式
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")
// 中文可读格式
DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH时mm分ss秒")
// ISO 8601格式
DateTimeFormatter.ISO_OFFSET_DATE_TIME
6. 异常处理与边界情况
6.1 非法时间戳处理
java复制public static String safeConvert(long timestamp) {
try {
if (timestamp < 0) {
throw new IllegalArgumentException("时间戳不能为负数");
}
return formatter.format(Instant.ofEpochMilli(timestamp));
} catch (DateTimeException e) {
log.error("时间转换异常", e);
return "无效时间";
}
}
6.2 未来时间限制
java复制public static String convertWithCheck(long timestamp) {
Instant instant = Instant.ofEpochMilli(timestamp);
if (instant.isAfter(Instant.now().plus(365, ChronoUnit.DAYS))) {
return "未来时间";
}
return formatter.format(instant);
}
7. 实际项目中的应用建议
-
DTO层转换:
java复制@Getter @Setter public class OrderDTO { private String orderId; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date createTime; } -
MyBatis类型处理器:
java复制public class TimestampHandler extends BaseTypeHandler<String> { private static final DateTimeFormatter formatter = //... @Override public String getNullableResult(ResultSet rs, String column) { long timestamp = rs.getLong(column); return timestamp == 0 ? null : formatter.format(Instant.ofEpochMilli(timestamp)); } } -
日志打印优化:
java复制long start = System.currentTimeMillis(); // 业务逻辑... log.info("耗时:{}", formatDuration(System.currentTimeMillis() - start)); private String formatDuration(long millis) { return String.format("%d分%d秒%d毫秒", millis / 60000, (millis % 60000) / 1000, millis % 1000); }
8. 单元测试要点
完整的测试应该覆盖:
java复制@Test
public void testLongToDateString() {
// 正常情况
assertEquals("2023-07-15 00:00:00",
DateUtils.longToDateString(1689350400000L));
// 边界值(1970年)
assertEquals("1970-01-01 08:00:00",
DateUtils.longToDateString(0L));
// 时区转换
assertEquals("2023-07-15 08:00:00",
DateUtils.longToDateString(1689388800000L, "UTC"));
// 异常输入
assertThrows(IllegalArgumentException.class,
() -> DateUtils.longToDateString(-1L));
}
9. 常见问题排查
问题1:转换结果比预期少8小时
- 原因:未考虑时区(中国是UTC+8)
- 解决:明确设置时区
ZoneId.of("Asia/Shanghai")
问题2:多线程环境下日期错乱
- 原因:共享了SimpleDateFormat实例
- 解决:改用ThreadLocal或DateTimeFormatter
问题3:毫秒数被截断
- 原因:使用了不包含毫秒的模式
- 解决:添加SSS模式字符
问题4:性能瓶颈
- 现象:批量转换时速度慢
- 优化:预编译Formatter对象,考虑并行流处理
10. 扩展思考
-
国际化处理:
java复制DateTimeFormatter formatter = DateTimeFormatter .ofLocalizedDateTime(FormatStyle.MEDIUM) .withLocale(Locale.CHINA); -
时间差计算:
java复制
Duration.between(startInstant, endInstant).toMillis(); -
高精度时间:
java复制Instant.now().toEpochMilli(); // 毫秒 Instant.now().getEpochSecond(); // 秒
在电商秒杀系统中,我们曾遇到时间同步问题:服务器时间不同步导致提前开卖。最终解决方案是统一使用NTP服务同步时间,所有业务逻辑基于System.currentTimeMillis()判断,转换为字符串仅用于展示。