都什么年代了你还在用 Date
共 3847字,需浏览 8分钟
·
2022-01-12 16:32
前言
上篇文章搞清楚了时区,这篇文章就主要来谈一谈 Java 中处理日期时间用什么 API 比较好。我本来不准备写这篇文章的,因为我觉得 Java17 都特么出来了,大家对 Java8 提供的时间日期 API 都很熟悉了。但是经过我调研,很多中小公司还在用老版本的 Date
来处理时间日期,视 Java8 提供的时间日期 API 于无物,所以还是想来推荐一下新一代的时间日期 API,希望对大家有帮助。
传统的 Date
老版本的 Date
相信大家都很熟悉了,这里就简单介绍几个点
可观不可触的时区
对于老版本的 Date
、SimpleDateFormat
相信大家都很熟悉,值得注意的是,你是无法直接设置 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(2019, 11, 30, 15, 16, 17);//指定日期时间
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
第一眼看到这两个类是不是想起了熟悉的 Collection
和 Collections
,与之类似这两个类是时间矫正器接口和时间矫正器的工具类。新版日期时间类几乎都实现了 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
的。所以还需要我告诉你选谁吗~~
结语
人们总是对于自己熟悉的东西持有倾向,对于不熟悉的新事物往往会抵触,曾经我也不止一次的抵触我亲爱的架构师让我们更换新的技术组件,但后来我都爱上了这些新的技术。
抵触新技术不是一个优秀程序员该有,这会阻碍你的成长。新技术的出现往往是弥补老技术的缺陷,没有哪个组织会花费人力物力出一个废物组件......所以每当有新技术组件出现时,请尝试它,也许会有意想不到的收获!