关于java:一个时区偏移转换导致的问题

88次阅读

共计 3150 个字符,预计需要花费 8 分钟才能阅读完成。

最近线上遇到一个比拟奇怪的问题,查下来还是挺有意思的,奇怪的常识又减少了。

线上服务有一个性能是从身份证号判断是否成年,具体做法是从身份证号截取出出生年月,而后判断是否大于 18 岁。性能比较简单,始终也没什么问题,直到昨天遇到一个日期解析的异样。

org.joda.time.IllegalInstantException: Cannot parse "19470415": Illegal instant due to time zone offset transition (Asia/Shanghai) 
at org.joda.time.format.DateTimeParserBucket.computeMillis(DateTimeParserBucket.java:473) 
at org.joda.time.format.DateTimeParserBucket.computeMillis(DateTimeParserBucket.java:411) 
at org.joda.time.format.DateTimeFormatter.parseDateTime(DateTimeFormatter.java:928)
......

19470415这个字符串格局正确,但用 joda 解析的时候就抛了异样。如下是解析的代码:

DateTime birthTime = DateTimeFormat.forPattern("yyyyMMdd").parseDateTime(birth);
DateTime adultTime = DateTime.now().plusYears(-18);
return adultTime.compareTo(birthTime) == 1;

从异样信息上看,预计是和时区无关。网上搜寻了下,在 joda 的文档中曾经写明了如何解决:

DateTime 换成 LocalDateTime 后果然解决了。

LocalDateTime birthTime = DateTimeFormat.forPattern("yyyyMMdd").parseLocalDateTime(birth);
LocalDateTime adultTime = LocalDateTime.now().plusYears(-18);
return adultTime.compareTo(birthTime) == 1;

解决后我开始尝试探索这背地的深层起因。

我尝试从 1990 年 1 月 1 日 到今日,一一尝试进行日期的解析,后果也很奇怪,只有几天会呈现这种异样:

19400601 parse fail, Cannot parse "19400601": Illegal instant due to time zone offset transition (Asia/Shanghai)
19410315 parse fail, Cannot parse "19410315": Illegal instant due to time zone offset transition (Asia/Shanghai)
19420131 parse fail, Cannot parse "19420131": Illegal instant due to time zone offset transition (Asia/Shanghai)
19460515 parse fail, Cannot parse "19460515": Illegal instant due to time zone offset transition (Asia/Shanghai)
19470415 parse fail, Cannot parse "19470415": Illegal instant due to time zone offset transition (Asia/Shanghai)
19480501 parse fail, Cannot parse "19480501": Illegal instant due to time zone offset transition (Asia/Shanghai)
19490501 parse fail, Cannot parse "19490501": Illegal instant due to time zone offset transition (Asia/Shanghai)

这几天到底有什么不一样,才会如此的非凡?这所有的背地到底是兽性的扭曲,还是道德的沦丧?

异样抛出的地位在 jodaDateTimeParserBucker类的 computeMillis 办法:

......

if (iOffset != null) {millis -= iOffset;} else if (iZone != null) {int offset = iZone.getOffsetFromLocal(millis);
    millis -= offset;
    if (offset != iZone.getOffset(millis)) {String message = "Illegal instant due to time zone offset transition (" + iZone + ')';
        if (text != null) {message = "Cannot parse \"" + text + "\": " + message;}
        throw new IllegalInstantException(message);
    }
}

......

millis是依据日期字符串计算出的一个工夫戳,iZone示意时区,不设置的话则用机器设置的时区,在上例中是 Asia/ShanghaigetOffsetFromLocal 办法返回的是一个偏移量,本地工夫减去这个偏移量就能够失去 UTC 工夫。getOffset办法返回的也是偏移量,UTC工夫加上这个偏移量就能够失去一个本地工夫。在下面 joda 的判断中,先调用 getOffsetFromLocal 从本地工夫 millis 计算一个偏移量 offsetmillis 减去 offset 后就是一个 UTC 工夫;再调用 getOffsetUTC工夫计算出一个偏移量,失常来讲,这两个偏移量应该是统一的,如果不统一就会抛出异样。

19470415 这个例子中,getOffsetFromLocal计算结果是 28800000 毫秒,即 8 个小时;getOffset办法计算结果则是 32400000 毫秒,即 9 个小时。这 1 个小时又是从何而来?

看下 getOffset 办法的正文:

getOffset返回的偏移量可能因为夏令时或政策起因而不同!

持续翻看 joda 的代码,在 org.joda.time.tz.src 中蕴含了一组时区相干的代码,这是来自 iana 的时区数据内容。iana Time Zone Database能够在这里下载。

咱们找到 jodaasia的时区数据,代码地址。以 Shanghai 为关键字,搜寻到这么一组数据:

这组日期看着有点眼生啊,这不就是之前日期解析异样那组日期吗?

细看后面的正文,大略意思就是历史上因为采纳过夏令时,或者和平等因素,导致某些历史上的某些时间段,Asia/Shanghai工夫与 UTC 工夫的时区距离可能不同。这段正文很有意思,援用连贯也能看到一些有意思的故事,比方 1919 年天津已经短暂的实施过夏令时制度,给老百姓折腾的够呛,起初又改回来了。

iana的时区数据怎么读呢?具体规定能够参考这里。最初一列的 D 示意 CDT,即夏令时;S 示意 CST,即规范时。大略猜想下这个数据的读法,以咱们服务触发的异样例子1947 年 04 月 15 日 为例,在 4 月 15 日的 0 点 0 分,采纳夏令时,工夫调整为 1 点。于是,相较于 UTC 工夫,多出了一个小时。

终于,咱们晓得这多的一个小时从何而来。晓得假相的我,真的想骂街。


题外话:夏令时真是个绝绝子的创造,如果是为了节约能源,明明批改作息就能解决的事,改来改去搞的很多货色都错乱了。

正文完
 0