当前位置: 首页 > 后端技术 > Java

时区偏移量转换引起的问题

时间:2023-04-01 21:00:35 Java

最近在网上遇到了一个奇怪的问题。查了下还是挺有意思的,奇怪的知识又增加了。在线服务的一个功能是根据身份证号码判断一个成年人是否是成年人。具体方法是从身份证号中截取出生日期,然后判断是否大于18岁。功能比较简单,一直没有问题,直到昨天遇到日期解析异常。org.joda.time.IllegalInstantException:无法解析“19470415”:由于时区偏移转换(亚洲/上海)在org.joda.time.format.DateTimeParserBucket.computeMillis(DateTimeParserBucket.java:473)在org.joda.time.format.DateTimeParserBucket.computeMillis(DateTimeParserBucket.java:411)atorg.joda.time.format.DateTimeFormatter.parseDateTime(DateTimeFormatter.java:928)......19470415此字符串格式正确,但使用joda解析时,抛出异常。解析代码如下:DateTimebirthTime=DateTimeFormat.forPattern("yyyyMMdd").parseDateTime(birth);DateTimeadultTime=DateTime.now().plusYears(-18);返回adultTime.compareTo(birthTime)==1;从异常信息来看,估计和时区有关。在网上搜索了一下,在joda的文档中已经写了如何解决:将DateTime换成LocalDateTime后就解决了。LocalDateTimebirthTime=DateTimeFormat.forPattern("yyyyMMdd").parseLocalDateTime(出生);LocalDateTimeadultTime=LocalDateTime.now().plusYears(-18);返回adultTime.compareTo(birthTime)==1;其背后的深层原因。我试着把1990年1月1日到今天的日期一一解析,结果也很奇怪,才几天就出现这个异常:19400601parsefail,Cannotparse"19400601":Illegalinstantduetotimezone偏移转换(亚洲/上海)19410315解析失败,无法解析“19410315”:由于时区偏移转换(亚洲/上海)19420131解析失败,无法解析“19420131”:由于时区转换导致的非法瞬间(亚洲/Shanghai)19460515parsefail,Cannotparse"19460515":Illegalinstantduetotimezoneoffsettransition(Asia/Shanghai)19470415parsefail,Cannotparse"19470415":Illegalinstantduetotimezoneoffsettransition(Asia/Shanghai)1980解析fail,Cannotparse"19480501":Illegalinstantduetotimezoneoffsettransition(Asia/Shanghai)19490501parsefail,Cannotparse"19490501":Illegalinstantduetotimezoneoffsettransition(Asia/Shanghai)它有什么不同很特别?这一切的背后,是人性的扭曲,还是道德的沦丧?抛出异常的位置是joda中DateTimeParserBucker类的computeMillis方法:......if(iOffset!=null){millis-=iOffset;}elseif(iZone!=null){intoffset=iZone.getOffsetFromLocal(millis);毫秒-=偏移量;if(offset!=iZone.getOffset(millis)){Stringmessage="由于时区偏移转换导致的非法瞬间("+iZone+')';if(text!=null){message="无法解析\""+text+"\":"+message;}抛出新的IllegalInstantException(消息);}}......millis是根据日期字符串计算的时间戳,iZone表示时区,不设置则使用机器设置的时区,上例中Asia/ShanghaigetOffsetFromLocal方法返回一个偏移量,本地时间减去这个偏移量就可以得到UTC时间。getOffset方法也返回一个偏移量,UTC时间加上这个偏移量就可以得到一个本地时间。在上面joda的判断中,首先调用getOffsetFromLocal计算一个offset,偏移量是本地时间millis,millis减去offset就是一个UTC时间;然后调用getOffset计算一个相对于UTC时间的偏移量,正常情况下,这两个偏移量应该是一致的,不一致会抛出异常。在19470415的例子中,getOffsetFromLocal的计算结果是28800000毫秒,即8小时;getOffset方法的计算结果是32400000毫秒,即9小时。这1小时是从哪里来的?看getOffset方法的注释:getOffset返回的offset可能会因为夏令时或者政策原因而不同!继续看joda的代码。org.joda.time.tz.src中包含了一组时区相关的代码,是来自iana的时区数据内容。iana时区数据库可在此处下载。我们在joda中找到了asia的时区数据和代码地址。以上海为关键词,搜索了这样一组数据:这组日期看着眼熟,这不就是之前日期解析异常的那组日期吗?仔细看前面的注释,大概意思是亚洲/上海时间和UTC时间之间的时区间隔在某些历史时期可能因为采用夏令时或者历史上发生战争而不同。这个评论很有意思,在参考链接中也可以看到一些有趣的故事。比如1919年,天津曾短暂实行夏令时制,给老百姓带来了很多麻烦,后来又改回来了。如何读取iana的时区数据?可以在此处找到详细规则。最后一栏的D表示CDT,即夏令时;S表示CST,即标准时间。我们大致猜猜这个数据的读取方式。以1947年4月15日我们服务触发的异常为例。4月15日0:00采用夏令时,时间调整为1:00。因此,与UTC时间相比,它多了一个小时。最后,我们知道额外的一小时是从哪里来的。知道真相的我真想骂街。题外话:夏令时真是一个完美的发明。如果是为了节约能源,可以通过修改作息时间来解决,但很多事情都搞砸了。