2017年元旦假期三天, 程序员小吴没有出去玩. 早上10点多了, 还在家睡懒觉. 突然电话响起,吵醒了半睡半醒的他. 他心想是推销还是贷款专员? 休假都被叫醒, 很是生气. 拿起手机, 发现竟然是公司打来的电话,顿时感觉不妙. 接通电话, 那头说有个紧急的site issue, 可能跟小吴的代码相关, 让他紧急上线看一下. 小吴一下子清醒了很多, 连忙登录VPN, 检查最新邮件, 确实发现有个问题, 跟自己几个月前上线的代码有关系.
这段代码巨简单, 就是取服务器当前的时间, 然后格式化一下, 显示在页面上. 同时,页面上有些数据统计信息, 会通过Ajax请求的方式去慢慢取,然后展示在页面. 去取的时候, 把本来展示在页面的这个格式化后的日期, 发给后台服务器. 问题就出现在, 本来这天是2017年12月31日, 可是页面上却显示的是 2018/12/31, 默认加了1年, 非常的奇怪.
小吴首先把线上已部署的代码的commit id 找到,然后把这个commit拉下来, 仔细检查了一番, 并没有发现什么情况. 这段代码已经在生产环境运行了8个多月, 之后一直没有人改过. 之前的测试以及线上从来没出现过问题, 真是奇了怪了. 小吴只好本地debug了一下, 发现本地能完美重现... 用当天的日期格式化一把, 格式化之后一直是2018年12月31日. 之前从来没有问题, 难道这天是啥好日子? 经过一番修改测试, 终于发现了问题:
public static void main(String[] args) {
Calendar cal = Calendar.getInstance();
cal.set(2017, 11, 29);
SimpleDateFormat sdf0 = new SimpleDateFormat("YYYY/MM/dd");
SimpleDateFormat sdf1 = new SimpleDateFormat("yyyy/MM/dd");
Date date = null;
for (int i = 0; i < 4; i++) {
date = cal.getTime();
System.out.println(sdf0.format(date));
System.out.println(sdf1.format(date));
cal.add(Calendar.DATE, 1);
}
}
运行结果:
2017/12/29
2017/12/29
2017/12/30
2017/12/30
2018/12/31
2017/12/31
2018/01/01
2018/01/01
从上面代码片段及运行结果看, 只有2017年12月31日出问题了. 问题出现在格式化的时候, 使用是大写的Y还是小写的y. 那么根据官方文档, 差别是什么呢?
y: Year Year 如: 1996; 96
Y: Week year Year 如: 2009; 09
一个是year, 一个 week year. year 很好理解, 就是我们所说的年, 那么 week year 呢? week year的计算涉及到2个东西, 一个是: 一周的第一天是从周一开始算还是从周日开始算, 有的人/地方从周日开始算, 有的从周一开始算. 第二个是, 我们经常听说这是今年第几周, 试想如果一个week跨过1年, 那么这周算到那年里?
JDK 文档举的例子是: 1998年1月1日是周四.
如果我们设定一周的第一天是周一, 那么这周属于1998年的就有4天(周4,5,6,7), 属于1997年的只有3天, 那么这周算在1998年,这是1998年的第一周. 那么1997年12月29, 30, 31日的 week year 就是1998, 而不是1997了.
如果我们设定一周的第一天是周日, 那么这周属于1998年的就有3天(周4,5,6), 属于1997年的就有4天了. 那么这周算在1997年,这是1997年的最后一周,1998年的第一周从1月4号开始, 那么1998年,1月1,2.3号的 week year 就是1997了.
FirstDayOfWeek 是根据JDK所在的系统设置, 有个默认设置的.
所以这个问题, 只要改回正确的yyyy问题就解决了. 另外还发现有人误以为这是JDK的bug, 去提了bug: https://bugs.openjdk.java.net/browse/JDK-8194625
参考:
https://docs.oracle.com/javase/7/docs/api/java/text/SimpleDateFormat.html
https://docs.oracle.com/javase/9/docs/api/java/util/GregorianCalendar.html#week_year