必须用 ZonedDateTime 或 Instant,而非 LocalDateTime;涉及跨时区、同步、提醒等场景时,LocalDateTime 会丢失时区上下文导致错误。

用 LocalDateTime 还是 ZonedDateTime?别默认选前者
个人日程管理工具的核心是时间点的准确表达与跨时区一致性。如果只用 LocalDateTime,它不带时区信息,保存到数据库或同步到手机时会丢失上下文——比如你在北京设了“明天9点开会”,导出到纽约设备上仍显示“明天9点”,实际可能已错过。必须根据场景选型:
- 纯本地单机使用(不联网、不跨设备)→ 可用
LocalDateTime,但需在 UI 明确标注“本地时间” - 涉及提醒、导出、多端同步 → 必须用
ZonedDateTime或存为Instant(推荐后者,更利于存储和计算) - 用户可切换时区(如出差模式)→ 用
ZonedDateTime记录原始输入,同时存一份Instant用于比对和触发
常见错误:用 new Date() 或 Calendar 构造事件,导致夏令时跳变、线程不安全、序列化异常。
java.time.temporal.ChronoUnit 是日程偏移计算的唯一可靠方式
日程常需“推迟2小时”“提前1天”“每周一重复”等操作。别用毫秒加减或手动算天数——ChronoUnit 才能正确处理月份天数不均、闰年、夏令时过渡等问题。
ZonedDateTime now = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));
ZonedDateTime nextWeek = now.plus(1, ChronoUnit.WEEKS); // 自动跨月、跨年
ZonedDateTime nextMonday = now.with(TemporalAdjusters.next(DayOfWeek.MONDAY));
注意:plusDays(7) 和 plus(7, ChronoUnit.DAYS) 行为一致,但 plusMonths(1) 比 plus(30, ChronoUnit.DAYS) 更语义清晰且结果准确(例如1月31日 +1月 = 2月28日,而非3月2日)。
立即学习“Java免费学习笔记(深入)”;
容易踩的坑:用 LocalDateTime.plusHours() 处理跨时区提醒,结果在夏令时切换日可能偏差1小时。
数据库存 Instant,前端传 ISO 8601 字符串,中间不碰字符串解析
MySQL 的 TIMESTAMP、PostgreSQL 的 TIMESTAMPTZ、H2 的 TIMESTAMP WITH TIME ZONE 都原生支持 Instant。Java 层统一用 Instant 存取,避免任何 SimpleDateFormat 或 DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")。
- 入库前:调用
zonedDateTime.toInstant() - 查库后:用
Instant.atZone(ZoneId.systemDefault())转回本地视图(UI 展示) - API 输入:接收标准 ISO 格式如
"2025-04-05T14:30:00+08:00",用ZonedDateTime.parse()解析 - 绝对不要写
LocalDateTime.parse("2025/04/05 14:30", DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm"))—— 格式脆弱、无时区、无法校验
Spring Boot 用户注意:@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") 会丢时区;应改用 @JsonFormat(shape = JsonFormat.Shape.STRING),让 Jackson 默认走 Instant 或 ZonedDateTime 的 ISO 序列化逻辑。
重复日程用 TemporalAdjuster 而不是 cron 表达式
个人工具不需要 Quartz 或 cron 的复杂语法。Java 8+ 的 TemporalAdjusters 已覆盖全部日常场景:
// 每周三下午3点
LocalDateTime base = LocalDateTime.of(2025, 4, 1, 15, 0);
LocalDateTime next = base.with(TemporalAdjusters.nextOrSame(DayOfWeek.WEDNESDAY))
.with(LocalTime.of(15, 0));
// 每月最后一个工作日
LocalDateTime lastWorkday = base.with(TemporalAdjusters.lastInMonth(DayOfWeek.FRIDAY));
关键点:
- 所有重复逻辑应在 Java 层计算,而不是存 cron 字符串再用第三方库解析——增加依赖、引入安全风险、难以调试
- 避免“每7天”这种粗粒度周期,它不等于“每周一”,会在跨月时错位
- 每月第N个某日(如“每月第二个周六”)用
TemporalAdjusters.dayOfWeekInMonth(2, DayOfWeek.SATURDAY)
真正难的是处理例外:某次重复被取消、某次时间被修改。这时需设计“主事件 + 实例覆盖”结构,而不是硬编码规则。










