写点什么

瞧瞧别人家的日期处理,那叫一个优雅!

  • 2025-04-23
    福建
  • 本文字数:2811 字

    阅读完需:约 9 分钟

前言


在我们的日常工作中,需要经常处理各种格式,各种类似的的日期或者时间。


比如:2025-04-21、2025/04/21、2025 年 04 月 21 日等等。


有些字段是 String 类型,有些是 Date 类型,有些是 Long 类型。


如果不同的数据类型,经常需要相互转换,如果处理不好,可能会出现很多意想不到的问题。


这篇文章跟大家一起聊聊日期处理的常见问题,和相关的解决方案,希望对你会有所帮助。


一、日期的坑


1.1 日期格式化陷阱


在文章的开头,先给大家列举一个非常经典的日期格式化问题:


// 旧代码片段(线程不安全的经典写法)public class OrderService {
private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");*
public void saveOrder(Order order) { // 线程A和线程B同时进入该方法 String createTime = sdf.format(order.getCreateTime()); // 可能出现"2023-02-30 12:00:00"这种根本不存在的日期 orderDao.insert(createTime);** }
}
复制代码


问题复现场景:


  1. 高并发秒杀场景下,10 个线程同时处理订单。

  2. 每个线程获取到的 order.getCreateTime()均为 2023-02-28 23:59:59。

  3. 由于线程调度顺序问题,某个线程执行 sdf.format()时。

  4. 内部 Calendar 实例已被其他线程修改为非法状态。

  5. 最终数据库中出现 2023-02-30 这类无效日期。


问题根源:SimpleDateFormat 内部使用了共享的 Calendar 实例,多线程并发修改会导致数据污染。


1.2 时区转换


我们在处理日期的时候,还可能会遇到夏令时转换的问题:


// 错误示范:简单加减8小时public Date convertToBeijingTime(Date utcDate) {    Calendar cal = Calendar.getInstance();    cal.setTime(utcDate);    cal.add(Calendar.HOUR, 8); // 没考虑夏令时切换问题    return cal.getTime();}
复制代码


夏令时是一种在夏季期间将时间提前一小时的制度,旨在充分利用日光,病节约能源。


在一些国家和地区,夏令时的开始和结束时间是固定的。


而在一些国家和地区,可能会根据需要调整。


在编程中,我们经常需要处理夏令时转换的问题,以确保时间的正确性。


隐患分析:2024 年 10 月 27 日北京时间凌晨 2 点会突然跳回 1 点,直接导致订单时间计算错误


二、优雅方案的进阶之路


2.1 线程安全重构


在 Java8 之前,一般是通过 ThreadLocal 解决多线程场景下,日期转换的问题。


例如下面这样:


// ThreadLocal封装方案(适用于JDK7及以下)public class SafeDateFormatter {    private static final ThreadLocal<DateFormat> THREAD_LOCAL = ThreadLocal.withInitial(() ->         new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")    );
public static String format(Date date) { return THREAD_LOCAL.get().format(date); }}
复制代码


线程安全原理:


  1. 每个线程第一次调用 format()方法时

  2. 会通过 withInitial()初始化方法创建独立的 DateFormat 实例

  3. 后续该线程再次调用时直接复用已有实例

  4. 线程销毁时会自动清理 ThreadLocal 存储的实例


原理揭秘:通过 ThreadLocal 为每个线程分配独立 DateFormat 实例,彻底规避线程安全问题。


2.2 Java8 时间 API 革命


在 Java8 之后,提供了 LocalDateTime 类对时间做转换,它是官方推荐的方案。


例如下面这样:


// 新时代写法(线程安全+表达式增强)public class ModernDateUtils {    public static String format(LocalDateTime dateTime) {        return dateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));    }
public static LocalDateTime parse(String str) { return LocalDateTime.parse(str, DateTimeFormatter.ISO_LOCAL_DATE_TIME); }}
复制代码


黑科技特性


  • 288 种预定义格式器

  • 支持 ISO-8601/ZonedDateTime 等国际化标准

  • 不可变对象天然线程安全


三、高阶场景解决方案


3.1 跨时区计算(跨国公司必备)


下面这个例子是基于时区计算营业时长:


// 正确示范:基于时区计算营业时长public Duration calculateBusinessHours(ZonedDateTime start, ZonedDateTime end) {    // 显式指定时区避免歧义    ZonedDateTime shanghaiStart = start.withZoneSameInstant(ZoneId.of("Asia/Shanghai"));    ZonedDateTime newYorkEnd = end.withZoneSameInstant(ZoneId.of("America/New_York"));        // 自动处理夏令时切换    return Duration.between(shanghaiStart, newYorkEnd);}
复制代码


底层原理:通过 ZoneId 维护完整的时区规则库(含历史变更数据),自动处理夏令时切换。


3.2 性能优化实战


日均亿级请求的处理方案:


// 预编译模式(性能提升300%)public class CachedDateFormatter {    private static final Map<String, DateTimeFormatter> CACHE = new ConcurrentHashMap<>();
public static DateTimeFormatter getFormatter(String pattern) { return CACHE.computeIfAbsent(pattern, DateTimeFormatter::ofPattern); }}
复制代码


我们可以使用 static final 这种预编译模式,来提升日期转换的性能。


性能对比



3.3 全局时区上下文+拦截器


为了方便统一解决时区问题,我们可以使用全局时区上下文+拦截器。


例如下面这样:


// 全局时区上下文传递public class TimeZoneContext {    private static final ThreadLocal<ZoneId> CONTEXT_HOLDER = new ThreadLocal<>();
public static void setTimeZone(ZoneId zoneId) { CONTEXT_HOLDER.set(zoneId); }
public static ZoneId getTimeZone() { return CONTEXT_HOLDER.get(); }}
// 在Spring Boot拦截器中设置时区@Componentpublic class TimeZoneInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String timeZoneId = request.getHeader("X-Time-Zone"); TimeZoneContext.setTimeZone(ZoneId.of(timeZoneId)); return true; }}
复制代码


此外,还需要在请求接口的 header 中传递 X-Time-Zone 时区参数。


四、优雅设计的底层逻辑


4.1 不可变性原则


// LocalDate的不可变设计LocalDate date = LocalDate.now();date.plusDays(1); // 返回新实例,原对象不变System.out.println(date); // 输出当前日期,不受影响
复制代码


4.2 函数式编程思维


// Stream API处理时间序列List<Transaction> transactions =     list.stream()        .filter(t -> t.getTimestamp().isAfter(yesterday)) // 声明式过滤        .sorted(Comparator.comparing(Transaction::getTimestamp)) // 自然排序        .collect(Collectors.toList()); // 延迟执行
复制代码


五、总结


下面总结一下日期处理的各种方案:



终极建议:在微服务架构中,建议建立统一的时间处理中间件,通过 AOP 拦截所有时间相关操作,彻底消除代码层面的时间处理差异。


文章转载自:苏三说技术

原文链接:https://www.cnblogs.com/12lisu/p/18840181

体验地址:http://www.jnpfsoft.com/?from=001YH

用户头像

还未添加个人签名 2023-06-19 加入

还未添加个人简介

评论

发布
暂无评论
瞧瞧别人家的日期处理,那叫一个优雅!_Java_不在线第一只蜗牛_InfoQ写作社区