关于LocalTime的坑

Shi Yi2026/2/15bagujava

背景

最近用LocalTime写了一个支付功能,本来以为只是个小功能,应该没啥问题,毕竟就是处理个时间嘛。在本地开发的时候一切正常,用的也是北京时间(UTC+8)。

但是!把这个代码部署到西部数码的ubuntu24服务器之后,给前端展示的时间数据出现了问题。排查了半天,才发现服务器的时区默认不是北京时间,而且这个西部数码的服务器镜像搞得也不太行,apt install get不到任何列表,而且还同时存在旧版本的apt镜像源配置文件和新版的apt配置文件,导致换源也会出现问题。

建议大家以后服务器还是用腾讯、阿里的这些,问题会少很多。毕竟我是那种连换源都能遇到问题的失败开发者...


问题分析

LocalTime的时区问题

LocalTime本身是不带时区的,它只表示一天中的时间(如 14:30:00)。但是当我们调用LocalTime.now()的时候,如果没有指定时区,它就会用系统的默认时区来获取当前时间。

TIP

这就解释了为什么在本地没问题,但部署到服务器后就不行了——两地时区不一样,获取到的时间也就不一样了

问题表现

  1. 本地开发时,时间显示正常
  2. 部署到服务器后,前端接收到的时间数据出现偏差
  3. 检查发现服务器默认时区不是 Asia/Shanghai(北京时间)

LocalTime vs Date:你应该选择哪个?

核心区别对比

特性java.util.Datejava.time.LocalTime
是否可变可变(线程不安全)不可变(线程安全)
时区处理包含时区但设计混乱明确区分带/不带时区
API设计旧API,设计有缺陷现代API,语义清晰
精度毫秒级纳秒级

Date 的设计缺陷

Date这个类确实有很多问题,但是我以前用代码生成器生成的多:

  1. 可变性导致线程不安全
Date date = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");

// 多线程环境下共享 sdf 和 date 会导致数据不一致
// 需要手动同步或使用 ThreadLocal
  1. 时区处理混乱
Date date = new Date();
// Date 内部存储的是 UTC 时间戳,但 toString() 使用系统默认时区
// 这种不一致性经常让开发者困惑
  1. 设计缺陷
// Date 的月份从 0 开始,年份从 1900 开始
Date date = new Date(2026 - 1900, 2 - 1, 15);  // 容易出错!
// 而且 Date 同时表示日期和时间,职责不清

LocalTime 的优势

LocalTime是Java 8引入的新时间API的一部分,设计上比Date好太多了:

// 创建 LocalTime(线程安全,API 清晰)
LocalTime time1 = LocalTime.of(14, 30, 0);  // 14:30:00
LocalTime time2 = LocalTime.now();          // 当前时间

// 支持 fluent API
LocalTime plusHours = time1.plusHours(2);    // 返回新对象,不修改原对象

// 时间计算
LocalTime start = LocalTime.of(9, 0);
LocalTime end = LocalTime.of(18, 0);
Duration duration = Duration.between(start, end);  // 9 hours

// 明确的时区处理(如果需要)
ZoneId zone = ZoneId.of("Asia/Shanghai");
LocalTime now = LocalTime.now(zone);

何时使用 LocalTime

适用场景:

  • 只需要表示一天中的某个时间点(如营业时间、班次时间)
  • 需要计算时间差、时间比较
  • 多线程环境下的时间处理
  • 需要精确到纳秒的时间

不适用场景:

  • 需要处理日期(用 LocalDate
  • 需要同时处理日期和时间(用 LocalDateTime
  • 需要处理带时区的完整时间(用 ZonedDateTime

何时使用 Date

WARNING

除非需要与遗留系统交互,否则完全不推荐使用 Date

唯一使用 Date 的场景:

  • 与使用旧版日期 API 的第三方库集成
  • 数据库类型映射为 java.sql.Date

迁移指南

如果你还在使用 Date,建议尽快迁移到新的时间 API:

// 旧代码(不推荐)
Date date = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
String timeStr = sdf.format(date);

// 新代码(推荐)
LocalTime time = LocalTime.now();
String timeStr = time.format(DateTimeFormatter.ofPattern("HH:mm:ss"));

// 两者可以互转(用于遗留系统集成)
Date legacyDate = Date.from(time.atDate(LocalDate.now())
    .atZone(ZoneId.systemDefault()).toInstant());

解决方案

方案一:设置服务器时区(推荐)

这是最彻底的解决方案,确保系统时区与业务要求一致。

# 查看当前时区
timedatectl status

# 设置时区为北京时间
sudo timedatectl set-timezone Asia/Shanghai

# 验证设置
timedatectl status

TIP

如果服务器镜像的 apt 源有问题,无法安装 tzdata,可以手动修改时区文件:

sudo cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime

方案二:在代码中显式指定时区

如果无法修改服务器设置,在代码中明确指定时区是最稳妥的方式:

// 获取北京时间的当前时间
ZoneId beijingZone = ZoneId.of("Asia/Shanghai");
LocalTime paymentTime = LocalTime.now(beijingZone);

// 对于完整的日期时间,使用 ZonedDateTime
ZonedDateTime now = ZonedDateTime.now(beijingZone);

// 如果只需要时间部分
LocalTime timeOnly = now.toLocalTime();

方案三:在应用启动时设置默认时区

对于整个应用都使用同一时区的情况,可以在启动时统一设置:

@SpringBootApplication
public class PaymentApplication {
    @PostConstruct
    public void init() {
        // 设置 JVM 默认时区
        TimeZone.setDefault(TimeZone.getTimeZone("Asia/Shanghai"));
        // 或者使用新的 API
        ZoneId.setDefault(ZoneId.of("Asia/Shanghai"));
    }
    
    public static void main(String[] args) {
        SpringApplication.run(PaymentApplication.class, args);
    }
}

WARNING

方案三会影响整个 JVM 进程,如果你的应用中有多租户或多时区需求,不推荐使用此方案


总结

  1. LocalTime 是 Java 8+ 的现代化时间 API,线程安全且设计合理
  2. 坚决弃用 Date 类,除非必须与遗留系统交互
  3. 时区问题在跨环境部署时必须重视,建议显式指定而非依赖系统默认
  4. 支付等敏感业务的时间处理要格外谨慎,考虑使用 ZonedDateTime 明确时区
  5. 服务器选型时考虑云服务商的镜像质量,避免基础设施问题影响开发效率

TIP

在涉及金融、支付等对时间准确性要求极高的业务时,强烈建议:

  • 使用数据库的 TIMESTAMP 类型存储完整时间戳
  • 在代码中显式指定业务时区
  • 考虑使用 Instant 作为跨系统的标准时间表示
  • 在日志中记录时区信息,便于问题排查
Last Updated 7/14/2026, 3:15:09 PM