前言
今年是2024年,刚好是闰年。大家都知道,闰年是有366天的,其中二月份有29天。最近作者有个项目组出了一个生产问题,跟闰年相关的。所以写篇文章跟大家讲讲这个bug,顺便讲讲Java日期处理的一些坑,让大家避坑~
1. 闰年处理的坑
这个bug是如何产生的呢?我给大家举个例子:
比如产品要求支持查询一年之间内的交易流水,并且对这个时间区间的检验。用户选了开始时间和结束时间后,前端先检验,检验通过后,传到后端。后端也检验,然后开始查询。
假设当天时间
2024.02.29
日,客户选的开始时间是2023.02.28
,结束时间是2024.02.29
.前端检验的时候,是用终止时间
2024.02.29
减12个月,得到开始时间2023.02.28
,因此认为区间是合法的后端检验的时候,用开始时间
2023.02.28
去加上12个月,得到2024.02.28
,即得到2023.02.28 ~ 2024.02.28
,因此认为这个时间范围是不合法的。这就前后端不一致了,产生bug了。
后端伪代码是这样实现的:
public class Test {
public static void main(String[] args) {
// 从前端获取的日期字符串
String startDateString = "20230228"; // 替换为从前端获取的startDate
String endDateString = "20240229"; // 替换为从前端获取的endDate
// 将字符串转换为LocalDate对象
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd");
LocalDate startDate = LocalDate.parse(startDateString, formatter);
LocalDate endDate = LocalDate.parse(endDateString, formatter);
// 将startDate增加12个月
LocalDate startDatePlus12Months = startDate.plusMonths(12);
// 检查是否在时间区间范围内
if (startDatePlus12Months.isBefore(endDate)) {
System.out.println("时间区间超过一年");
throw new RuntimeException();
} else {
System.out.println("时间区间小于一年");
}
}
}
运行结果抛出了一场,输出时间区间超过一年
其实这个例子,就是我们是否把闰年的02.29日这一天算进去,怎么理解都算合理,但是就是要避免前后端不一致的坑。
关于闰年处理的坑,我总结为主要是这几个方面:
-
在一个日期值上加或减时间的代码,比如像加减1年或1个月的代码
-
数据库查询,报表这些,月度和年度统计可能会少算 1 天
-
证书/密码/密钥/缓存 等的过期时间,可能会比预期的早了一天 固定长度数组,长度365时不够的
-
判断是否是闰年的方法, 4年一润,百年不润,四百年又润
小伙伴们在开发代码的时候,注意就好啦~
2. 日期格式yyyy-MM-dd HH:mm:ss 大小写含义区别
YYYY 和 yyyy 区别
-
年份大写YYYY: 当天所在的周属于的年份,一周从周日开始,周六结束,只要本周跨年,那么这周就算进入下一年。
-
yyyy: 表示四位年份。
比如看这个例子:
从输出结果,大家可以发现,同一个日期,只是转化了一下格式,日期就变了。就是因为年份YYYY
大写的问题。
HH 和 hh 区别
-
小时大写
HH
:代表24
小时制的小时。 -
hh
:是12制的日期格式,当时间为12
点,会处理为0
点。
3.SimleDateFormat的format初始化问题
4. DateTimeFormatter 日期本地化问题
反例:
String dateStr = "Wed Mar 18 10:00:00 2020";
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("EEE MMM dd HH:mm:ss yyyy");
LocalDateTime dateTime = LocalDateTime.parse(dateStr, formatter);
System.out.println(dateTime);
运行结果:
Exception in thread "main" java.time.format.DateTimeParseException: Text 'Wed Mar 18 10:00:00 2020' could not be parsed at index 0
at java.time.format.DateTimeFormatter.parseResolved0(DateTimeFormatter.java:1949)
at java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:1851)
at java.time.LocalDateTime.parse(LocalDateTime.java:492)
at com.example.demo.SynchronizedTest.main(SynchronizedTest.java:19)
解析:
DateTimeFormatter 这个类默认进行本地化设置,如果默认是中文,解析英文字符串就会报异常。可以传入一个本地化参数(Locale.US)解决这个问题
正例:
String dateStr = "Wed Mar 18 10:00:00 2020";
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("EEE MMM dd HH:mm:ss yyyy",Locale.US);
LocalDateTime dateTime = LocalDateTime.parse(dateStr, formatter);
System.out.println(dateTime);
5. Calendar设置时间的坑
反例:
Calendar c = Calendar.getInstance();
c.set(Calendar.HOUR, 10);
System.out.println(c.getTime());
运行结果:
Thu Mar 26 22:28:05 GMT+08:00 2020
解析:
我们设置了10小时,但运行结果是22点,而不是10点。因为Calendar.HOUR默认是按12小时制处理的,需要使用Calendar.HOUR_OF_DAY,因为它才是按24小时处理的。
Calendar c = Calendar.getInstance();
c.set(Calendar.HOUR_OF_DAY, 10);
6.日期计算的坑
日期计算,比如new Date()
加一个30
天,有些小伙伴可能会这么写:
得到的日期居然比当前日期还要早,根本不是晚 30 天的时间。这是因为发生了int
发生了溢出。
可以把30
修改为 30L
,或者直接用Java 8的加减API,如下:
7. SimpleDateFormat 线性安全问题
比如这个例子:
public class SimpleDateFormatTest {
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 100, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>(1000));
while (true) {
threadPoolExecutor.execute(() -> {
String dateString = sdf.format(new Date());
try {
Date parseDate = sdf.parse(dateString);
String dateString2 = sdf.format(parseDate);
System.out.println(dateString.equals(dateString2));
} catch (ParseException e) {
e.printStackTrace();
}
});
}
}
SimpleDateFormat
是个共享变量,多线程跑的时候,就会有问题:
Exception in thread "pool-1-thread-49" java.lang.NumberFormatException: For input string: "5151."
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Long.parseLong(Long.java:589)
at java.lang.Long.parseLong(Long.java:631)
at java.text.DigitList.getLong(DigitList.java:195)
at java.text.DecimalFormat.parse(DecimalFormat.java:2051)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2162)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at com.example.demo.SimpleDateFormatTest.lambda$main$0(SimpleDateFormatTest.java:19)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
Exception in thread "pool-1-thread-47" java.lang.NumberFormatException: For input string: "5151."
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Long.parseLong(Long.java:589)
at java.lang.Long.parseLong(Long.java:631)
at java.text.DigitList.getLong(DigitList.java:195)
at java.text.DecimalFormat.parse(DecimalFormat.java:2051)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2162)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at com.example.demo.SimpleDateFormatTest.lambda$main$0(SimpleDateFormatTest.java:19)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
或者直接使用DateTimeFromatter
8. Java日期的夏令时问题
反例:
TimeZone.setDefault(TimeZone.getTimeZone("Asia/Shanghai"));
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(sdf.parse("1986-05-04 00:30:00"));
运行结果:
Sun May 04 01:30:00 CDT 1986
解析:
先了解一下夏令时
-
夏令时,表示为了节约能源,人为规定时间的意思。
-
一般在天亮早的夏季人为将时间调快一小时,可以使人早起早睡,减少照明量,以充分利用光照资源,从而节约照明用电。
-
各个采纳夏时制的国家具体规定不同。目前全世界有近
110
个国家每年要实行夏令时。 -
1986年4月
,中国中央有关部门发出“在全国范围内实行夏时制的通知”,具体作法是:每年从四月中旬第一个星期日的凌晨2时整(北京时间),将时钟拨快一小时。(1992
年起,夏令时暂停实行。) -
夏时令这几个时间可以注意一下哈,
1986-05-04, 1987-04-12, 1988-04-10, 1989-04-16, 1990-04-15, 1991-04-14
。
结合demo
代码,中国在1986-05-04
当天还在使用夏令时,时间被拨快了1
个小时。所以0点30分
打印成了1点30分
。如果要打印正确的时间,可以考虑修改时区为东8区。
正例:
TimeZone.setDefault(TimeZone.getTimeZone("GMT+8"));
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(sdf.parse("1986-05-04 00:30:00"));