都什么年代了你还在用 Date

艾小仙

共 3847字,需浏览 8分钟

 · 2022-01-12

前言

上篇文章搞清楚了时区,这篇文章就主要来谈一谈 Java 中处理日期时间用什么 API 比较好。我本来不准备写这篇文章的,因为我觉得 Java17 都特么出来了,大家对 Java8 提供的时间日期 API 都很熟悉了。但是经过我调研,很多中小公司还在用老版本的 Date 来处理时间日期,视 Java8 提供的时间日期 API 于无物,所以还是想来推荐一下新一代的时间日期 API,希望对大家有帮助。

传统的 Date

老版本的 Date 相信大家都很熟悉了,这里就简单介绍几个点

可观不可触的时区

对于老版本的 DateSimpleDateFormat 相信大家都很熟悉,值得注意的是,你是无法直接设置 Date 的时区信息的,但与之矛盾的是我们在代码中从数据库读取一个带时区的时间,例如:2021-11-01 13:50:47.138494+00 ,它却能够自动解析成当前服务器所在时区的时间封装在 Date 对象中。其实这是它的一个成员变量 private transient BaseCalendar.Date cdate 去做的事情,由于 Unix 时间戳是和时区无关的,所以在从数据库读取时它会将数据库带有时区时间转换为 Unix 时间戳,然后在用这个时间戳转换为当前服务器所在时区的时间,并且携带着时区信息。

其实我们可以看一下 Date 构造方法源码,他就是获取的当前 Unix 时间戳。

public Date() {
    this(System.currentTimeMillis());
}

复杂的时区转换计算

如果我们需要显示一个 Date 对象在不同时区的时间,那么我们需要通过 SimpleDateFormat 来实现

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date date = new Date();//默认北京时区的时间
System.out.println("北京时区:" + sdf.getTimeZone());
System.out.println("北京时区时间:" + sdf.format(date));
sdf.setTimeZone(TimeZone.getTimeZone(ZoneId.of("Asia/Jakarta")));
System.out.println("雅加达时区:" + sdf.getTimeZone());
System.out.println("雅加达时区时间:" + sdf.format(date));

如果你想得到的不是字符串而是转换为另一个时区的 Date 对象,那么你还需要再从字符串反转到 Date ......这无疑是非常麻烦的

复杂时间间隔计算

要说 Date 最难受的地方之一就是两个日期的时间差计算,之前 JDK 并没有提供直接的 API 来计算两个 Date 的间隔。通常我们是把 Date 转成时间戳之后进行操作

Date date1 = new Date();
TimeUnit.SECONDS.sleep(10);
Date date2 = new Date();
long time1 = date1.getTime();
long time2 = date2.getTime();
System.out.println("间隔秒数:" + (time2 - time1) / 1000);
System.out.println("间隔小时数:" + (time2 - time1) / (1000 * 3600));
//...

是不是很麻烦?如果你并不觉得麻烦,那么你只是没有见过更好的方式。我们来看看新版 API 怎么来做(后面会详细介绍)

Duration duration = Duration.between(time2, time1);
long days = duration.toDays();//获取天数间隔
long hours = duration.toHours();//获取小时间隔
//...

这样是不是很简单!

数据库到底要不要存储时区信息

在谈新版的时间 API 之前,我们先要搞清楚一个问题,那就是你真的有必要把时区信息存储到数据库吗?其实我觉得对于绝大多数的公司应该都是不需要的,下我以我们公司印度尼西亚业务为例来分析这个问题。

存储时区

我们目前是先在代码中获取当前服务器(我们服务器在亚马逊 UTC-3 )所在时区的 Date 对象,在存入数据库时,在数据库层面将其转换为 UTC+8 时区的时间存储到数据库中,例如 2021-10-24 15:47:47.138494-03 存到数据库中是 2021-10-25 02:47:47.138494+08,然后在前端页面可以直接调用 API 根据带时区的时间计算出前端设备当地时区的时间。

xxx//假如设备在印度尼西亚,那么前端 API 获得的时间就是 2021-10-25 01:47:47.138494

不过不是很明白,我们既然存时区,那么应该也要存印度尼西亚的时区啊......毕竟我们的业务在印尼,也不在北京......

不存储时区

正常来说服务器所在地一般是唯一的,即使有 100 个服务实例来做负载均衡,总不可能出现一半服务器在美国,一半在中国吧?那么代码中的时间日期操作都是默认使用服务器所在地时区(UTC-3),既然如此那么我们数据库就可以不存储时区信息,只存储一个时间描述例如 2021-10-24 13:50:47.138494,它本身没有时区,但是我们都知道它的默认时区就是 UTC-3

这样在前端代码中只需要调用 API 的时候带上带上该时间的所属时区 UTC-3 即可算出前端设备当地时区的时间。

xxx//假如设备在印度尼西亚,那么前端 API 获得的时间就是 2021-10-25 01:47:47.138494

而且这个不带时区的时间不会受数据库时区的影响,无论你把数据库设置成哪个时区,它都不会变化。这种方式就是整个业务里我们把所有时区都干掉,只留一个服务器时区,当有任何涉及时区的业务功能时,只需要把源时间换算成服务器所在时区的时间即可。

总结

不存储时区还有一种情况就是有些程序员喜欢存时间戳,不得不吐槽一下我觉得这是最 low 的方式之一(也许你能说出它仅有的个别优点,但我不接受反驳)。虽然时间戳是和时区无关的,但是它的可读性真的太差了......而且代码中也不好进行操作

其实对比一下就能发现数据库存不存储时区信息,对于前后端的操作区别不大的。而且我觉得绝大多数业务场景,不存储时区相对更简单!

Java8 的时间日期 API

LocalDate、LocalTime、LocalDateTime

看源码是一个好习惯,看源码注释更是一个好习惯。这三个类的注释说的很清楚,首先这些类是线程安全的,对于它的任何操作都会产生一个新的实例,这和 String 类是一样的。其次它不存储或表示时区。相反,它是对日期时间的描述。

值得注意的是,它不存储时区,但不代表它没有时区,细品这句话!通过一张图来理解三者的关系

下面以 LocalDateTime 为例简单介绍下用法

LocalDateTime time1 = LocalDateTime.now();
LocalDateTime time2 = LocalDateTime.now();

time1.isAfter(time2); time1.isBefore(time2);//比较时间
time1.plusDays(1L); time1.minusHours(1L);//加减时间日期
LocalDateTime.parse("2021-11-19T15:16:17");//解析时间
LocalDateTime.of(20191130151617);//指定日期时间
LocalDateTime.now(ZoneId.of("Asia/Jakarta"));//其他时区相对此服务器时区的时间
//...

如果你可以不在数据库中存储时区信息的话,那么请使用这个类。如果你一定要存储,那么也请使用下面的 OffsetDateTime 而不要使用 Date

OffsetDateTime

带有时区偏移量的日期时间类,相当于 OffsetDateTime = LocalDateTime + ZoneOffset

大多数情况下我们使用带有偏移量的日期时间已经能够满足需求。

ZonedDateTime

真正带有完整时区信息的日期时间类

Duration、Period

这两个类是代表一段时间或者说是两个时间的间隔,以 Duration 为例,试想在 Java8 之前你有一个业务要表示一个令牌的有效期为 7 天,那么通常的做法可能是存储令牌的创建时间,然后在代码中用系统当前时间减去令牌创建时间和 7 天做比较,例如

long duration = System.currentTimeMillis() - token.getCreateTime().getTime();
if (duration < 7 * 24 * 60 * 60 * 1000) { //令牌合法}

但是在使用 Duration 之后,

Duration duration = Duration.between(token.getCreateTime(), LocalDateTime.now());
boolean negative = duration.minus(effective).isNegative();//是否过期

其次 Duration 在 SpringBoot 项目中,配置也很方便

token:
  effective-time: 7d  # d:天 , h:小时 , m:分钟 , s:秒

在实体类中,可以使用 @ConfigurationProperties 或者 @Value 将它直接映射成 Duration 对象,当然这依赖于 SpringBoot 中提供的丰富的类型转换器。下一篇文章会介绍

TemporalAdjuster 和 TemporalAdjusters

第一眼看到这两个类是不是想起了熟悉的 CollectionCollections ,与之类似这两个类是时间矫正器接口和时间矫正器的工具类。新版日期时间类几乎都实现了 TemporalAdjuster ,以便于针对所有日期时间都可以对其进行计算得到另外一个时间,例如

LocalDateTime.now().with(TemporalAdjusters.firstDayOfMonth());//当月第一天LocalDateTime.now().with(TemporalAdjusters.firstDayOfNextMonth());//下个月第一天LocalDateTime.now().with(TemporalAdjusters.dayOfWeekInMonth(2,DayOfWeek.MONDAY));//第N个星期几LocalDateTime.now().with(TemporalAdjusters.next(DayOfWeek.MONDAY));//下个星期几//...

有这么丰富的 API ,你还需要写一堆日期时间的工具类吗?

Date 和 LocalDateTime 互转

不可否认存在一种现象就是你的项目一直用的都是 Date,而 leader 又不愿意花费时间精力去升级,或者老的业务限制的情况,那么某些场景下你可以使用 Java8 提供的 Instant 将两者互转来简化一些业务代码操作

LocalDateTime.ofInstant(new Date().toInstant(), ZoneId.systemDefault());//Date 转 LocalDateTime

Date.from(LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant());// LocalDateTime 转 Date

总结

Java8 的时间日期工具还有很多用法,这里就不一一介绍了,总之 Date 有的功能 LocalDateTime 都有,Date 没有的功能,LocalDateTime 还有很多。使用新版的日期时间,几乎是不存在原来的 DateUtil 的。所以还需要我告诉你选谁吗~~

结语

人们总是对于自己熟悉的东西持有倾向,对于不熟悉的新事物往往会抵触,曾经我也不止一次的抵触我亲爱的架构师让我们更换新的技术组件,但后来我都爱上了这些新的技术。

抵触新技术不是一个优秀程序员该有,这会阻碍你的成长。新技术的出现往往是弥补老技术的缺陷,没有哪个组织会花费人力物力出一个废物组件......所以每当有新技术组件出现时,请尝试它,也许会有意想不到的收获!


浏览 42
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报