引言在Java 8正式推出JSR310时间日期API之前Java开发者长期被java.util.Date、java.util.Calendar、SimpleDateFormat等老旧API的各种反人类设计和坑点折磨比如月份从0开始、类可变性导致线程不安全、格式化器高并发下报错、时区处理混乱等。我所在的团队曾经就因为线上高并发场景下共用SimpleDateFormat实例导致千分之三的日期解析错误最终引发订单超时统计异常的生产事故。JSR310又被称为ThreeTen项目正是为了解决上述所有问题而生它借鉴了Joda-Time的优秀设计从JDK 8开始内置为官方标准API天生线程安全、设计符合人类直觉、功能覆盖所有日期时间操作场景。但即使是这么优秀的API很多开发者在实际使用过程中还是会踩到各种隐藏坑点本文就从核心原理、常用操作、生产避坑、工具封装四个维度给大家带来可以直接落地的完整指南。一、JSR310核心设计原理JSR310的优秀体验来自于它底层的4个核心设计原则从根源上避免了老API的所有问题1. 不可变类设计JSR310的所有核心日期时间类都是final修饰的不可变类所有修改操作都会返回一个新的对象实例不会修改原对象天生线程安全完全不需要加锁就能在多线程环境下共用。2. 域明确分离不再用一个Date类承担所有场景时间戳用Instant、本地日期用LocalDate、本地时间用LocalTime、带时区的日期时间用ZonedDateTime不同场景用对应的类避免歧义。3. 人类友好设计月份从1开始1代表1月、星期从1开始1代表周一7代表周日所有参数命名符合日常认知不会出现setMonth(8)实际代表9月这种反人类设计。4. 功能内聚所有日期操作加减、比较、格式化、调整都封装在对应类中不需要额外引入工具类同时格式化器DateTimeFormatter也是不可变类天生线程安全。二、核心组件详解与基础示例我们先对JSR310的常用核心组件做逐个讲解所有示例都可以直接复制运行import java.time.*; import java.time.format.DateTimeFormatter; import java.time.temporal.TemporalAdjusters; public class Jsr310Demo { public static void main(String[] args) { // 1. Instant时间戳精确到纳秒对应老API的Date存储的是UTC时区的时间 Instant nowInstant Instant.now(); System.out.println(当前时间戳 nowInstant); // 毫秒转Instant Instant instantOfMilli Instant.ofEpochMilli(System.currentTimeMillis()); // Instant转毫秒 long milli nowInstant.toEpochMilli(); // 2. LocalDate本地日期不带时间、不带时区 LocalDate nowDate LocalDate.now(); System.out.println(当前日期 nowDate); // 指定日期2024年5月20日 LocalDate specifyDate LocalDate.of(2024, 5, 20); // 获取年、月、日 int year specifyDate.getYear(); int month specifyDate.getMonthValue(); int day specifyDate.getDayOfMonth(); // 判断是否是闰年 boolean isLeapYear specifyDate.isLeapYear(); // 3. LocalTime本地时间不带日期、不带时区 LocalTime nowTime LocalTime.now(); System.out.println(当前时间 nowTime); // 指定时间14点30分0秒 LocalTime specifyTime LocalTime.of(14, 30, 0); // 4. LocalDateTime本地日期时间不带时区相当于LocalDateLocalTime LocalDateTime nowDateTime LocalDateTime.now(); System.out.println(当前日期时间 nowDateTime); // 组合创建 LocalDateTime specifyDateTime LocalDateTime.of(specifyDate, specifyTime); // 拆分日期和时间 LocalDate datePart specifyDateTime.toLocalDate(); LocalTime timePart specifyDateTime.toLocalTime(); // 5. ZoneId时区标识禁止用CST/EST这种缩写避免歧义推荐用区域ID格式 ZoneId shanghaiZone ZoneId.of(Asia/Shanghai); ZoneId newYorkZone ZoneId.of(America/New_York); // 6. ZonedDateTime带时区的日期时间 ZonedDateTime shanghaiDateTime ZonedDateTime.of(specifyDateTime, shanghaiZone); System.out.println(上海时区时间 shanghaiDateTime); // 时区转换转成纽约时间 ZonedDateTime newYorkDateTime shanghaiDateTime.withZoneSameInstant(newYorkZone); System.out.println(对应纽约时区时间 newYorkDateTime); // 7. 时间间隔计算 // Duration秒/纳秒级间隔适合计算时间差 LocalDateTime start LocalDateTime.of(2024, 5, 20, 14, 0); LocalDateTime end LocalDateTime.of(2024, 5, 20, 16, 30); Duration duration Duration.between(start, end); System.out.println(时间差小时数 duration.toHours()); System.out.println(时间差分钟数 duration.toMinutes()); // Period年/月/日级间隔适合计算日期差 LocalDate birthday LocalDate.of(2024, 10, 1); Period period Period.between(nowDate, birthday); System.out.println(距离生日还有 period.getMonths() 个月 period.getDays() 天); // 8. DateTimeFormatter格式化器线程安全 DateTimeFormatter formatter DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss); // 格式化 String formatStr nowDateTime.format(formatter); System.out.println(格式化后的时间 formatStr); // 解析 LocalDateTime parsedDateTime LocalDateTime.parse(2024-05-20 14:30:00, formatter); System.out.println(解析后的时间 parsedDateTime); } }三、常用业务场景实战下面是日常开发中最常用的几个业务场景的实现方式避免大家自己写逻辑踩坑1. 日期调整操作用内置的TemporalAdjusters工具类可以快速实现各种日期调整需求不需要自己写逻辑// 获取当月第一天 LocalDate firstDayOfMonth nowDate.with(TemporalAdjusters.firstDayOfMonth()); // 获取当月最后一天 LocalDate lastDayOfMonth nowDate.with(TemporalAdjusters.lastDayOfMonth()); // 获取下一个周一 LocalDate nextMonday nowDate.with(TemporalAdjusters.next(DayOfWeek.MONDAY)); // 获取当月第二个周日 LocalDate secondSundayOfMonth nowDate.with(TemporalAdjusters.dayOfWeekInMonth(2, DayOfWeek.SUNDAY));2. 时间戳与LocalDateTime互转注意互转必须指定时区否则会用系统默认时区不同环境部署会出现时间差// LocalDateTime转时间戳东8区 long timestamp LocalDateTime.now() .atZone(ZoneId.of(Asia/Shanghai)) .toInstant() .toEpochMilli(); // 时间戳转LocalDateTime东8区 LocalDateTime dateTime Instant.ofEpochMilli(timestamp) .atZone(ZoneId.of(Asia/Shanghai)) .toLocalDateTime();3. 判断时间是否在某个区间LocalTime startTime LocalTime.of(9, 0); LocalTime endTime LocalTime.of(18, 0); LocalTime now LocalTime.now(); // 判断当前是否在工作时间 boolean isWorkingTime !now.isBefore(startTime) !now.isAfter(endTime);四、生产级避坑指南JSR310虽然设计优秀但如果使用不当还是会踩到很多隐藏坑点下面是我们团队在生产环境踩过的7个典型坑坑1格式符yyyy和YYYY混用导致跨年日期错误yyyy是我们常用的公历年而YYYY是周年代表当周所在的年份每年的最后一周和第一周很容易出现年份错位// 反例2023年12月31日是周日属于2024年的第一周用YYYY会得到2024 LocalDate date LocalDate.of(2023, 12, 31); DateTimeFormatter wrongFormatter DateTimeFormatter.ofPattern(YYYY-MM-dd); System.out.println(date.format(wrongFormatter)); // 输出2024-12-31完全错误 // 正例用yyyy才是正确的公历年 DateTimeFormatter rightFormatter DateTimeFormatter.ofPattern(yyyy-MM-dd); System.out.println(date.format(rightFormatter)); // 输出2023-12-31正确坑2LocalDateTime转时间戳未指定时区导致时间差很多开发者转时间戳的时候直接用LocalDateTime.now().toInstant(ZoneOffset.UTC)如果系统时区是东8区就会少8小时正确做法是统一指定业务时区// 统一配置业务时区为东8区建议抽成常量 private static final ZoneId BUSINESS_ZONE ZoneId.of(Asia/Shanghai); // 正例 long correctTimestamp LocalDateTime.now().atZone(BUSINESS_ZONE).toInstant().toEpochMilli();坑3夏令时场景直接加减小时导致时间错误欧洲、北美等很多国家有夏令时制度每年会有一天少1小时、另一天多1小时如果直接用plusHours(24)计算次日时间会出现时间偏移// 反例纽约2024年3月10日是夏令时开始日期凌晨2点直接跳成3点少1小时 ZonedDateTime nyTime ZonedDateTime.of(2024, 3, 9, 12, 0, 0, 0, ZoneId.of(America/New_York)); ZonedDateTime wrongNextDay nyTime.plusHours(24); // 得到的是3月10日13点不是12点 // 正例用plusDays(1)会自动处理夏令时偏移 ZonedDateTime rightNextDay nyTime.plusDays(1); // 得到的是3月10日12点正确坑4Jackson序列化JSR310类默认返回数组默认情况下Jackson序列化LocalDateTime会返回[2024,5,20,14,30,0]这种数组格式前端无法解析需要引入序列化模块并配置引入依赖SpringBoot 2.4已经自动引入dependency groupIdcom.fasterxml.jackson.datatype/groupId artifactIdjackson-datatype-jsr310/artifactId /dependency全局配置SpringBoot中直接加在application.ymlspring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai serialization: write-dates-as-timestamps: false坑5新旧API互转未指定时区如果老系统还有用Date的场景互转的时候必须指定时区否则会出现偏移// Date转LocalDateTime 正例 public LocalDateTime dateToLocalDateTime(Date date) { return date.toInstant().atZone(BUSINESS_ZONE).toLocalDateTime(); } // LocalDateTime转Date 正例 public Date localDateTimeToDate(LocalDateTime dateTime) { return Date.from(dateTime.atZone(BUSINESS_ZONE).toInstant()); }五、生产级工具类封装我们把常用操作封装成了线程安全的工具类大家可以直接在项目中使用import java.time.*; import java.time.format.DateTimeFormatter; import java.time.temporal.TemporalAdjusters; import java.util.Date; public final class DateTimeUtils { // 统一业务时区 public static final ZoneId BUSINESS_ZONE ZoneId.of(Asia/Shanghai); // 常用格式化器线程安全可以全局共用 public static final DateTimeFormatter DATETIME_FORMATTER DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss); public static final DateTimeFormatter DATE_FORMATTER DateTimeFormatter.ofPattern(yyyy-MM-dd); public static final DateTimeFormatter TIME_FORMATTER DateTimeFormatter.ofPattern(HH:mm:ss); private DateTimeUtils() {} // 1. 格式化 public static String formatDateTime(LocalDateTime dateTime) { return dateTime.format(DATETIME_FORMATTER); } public static String formatDate(LocalDate date) { return date.format(DATE_FORMATTER); } // 2. 解析 public static LocalDateTime parseDateTime(String dateTimeStr) { return LocalDateTime.parse(dateTimeStr, DATETIME_FORMATTER); } public static LocalDate parseDate(String dateStr) { return LocalDate.parse(dateStr, DATE_FORMATTER); } // 3. 时间戳互转 public static long toTimestamp(LocalDateTime dateTime) { return dateTime.atZone(BUSINESS_ZONE).toInstant().toEpochMilli(); } public static LocalDateTime toDateTime(long timestamp) { return Instant.ofEpochMilli(timestamp).atZone(BUSINESS_ZONE).toLocalDateTime(); } // 4. 获取当月第一天 public static LocalDate getFirstDayOfMonth() { return LocalDate.now(BUSINESS_ZONE).with(TemporalAdjusters.firstDayOfMonth()); } // 5. 获取当月最后一天 public static LocalDate getLastDayOfMonth() { return LocalDate.now(BUSINESS_ZONE).with(TemporalAdjusters.lastDayOfMonth()); } // 6. 计算两个日期的间隔天数 public static long daysBetween(LocalDate start, LocalDate end) { return end.toEpochDay() - start.toEpochDay(); } // 7. 新旧API互转 public static LocalDateTime dateToDateTime(Date date) { return date.toInstant().atZone(BUSINESS_ZONE).toLocalDateTime(); } public static Date dateTimeToDate(LocalDateTime dateTime) { return Date.from(dateTime.atZone(BUSINESS_ZONE).toInstant()); } // 8. 判断是否是工作时间9点-18点 public static boolean isWorkingTime() { LocalTime now LocalTime.now(BUSINESS_ZONE); return !now.isBefore(LocalTime.of(9, 0)) !now.isAfter(LocalTime.of(18, 0)); } }总结JSR310时间日期API已经推出了近10年目前所有主流框架SpringBoot、MyBatis、Redis等都已经完美支持强烈建议所有还在使用Date、Calendar、SimpleDateFormat的团队彻底替换成JSR310 API只要遵循本文的避坑指南完全可以避免99%的日期时间相关的线上问题。如果大家有其他遇到的日期相关坑点也欢迎在评论区留言交流。