1.问题说明
最近在给代码写单元测试,遇到了一个比较有趣的问题,问题描述如下图。
Parse Exception
java.text.ParseException: Unparseable date: "2018-04-08T21:45:00+08:00"
这是一个日期解析的错误,场景是利用Mockito和JUnit进行Android的单元测试时发生的。之所以要记录这个问题是因为在StackoverFlow上并没有看到太好的solution,所以我调查后成文,记录一下。
2.问题原因
问题的root在这里(方法调用顺序就是代码罗列顺序):
// MeetingListBasicPresenterTest.java
// 在这个测试类的setup()方法中,需要创建一个MeetingInfoProxy的对象 // 因为MeetingInfoProxy是代理类,所以需要代理一个meeting对象,所以在inject(...)方法中传入一个meeting对象。 ... @Before public void setup() { MockitoAnnotations.initMocks(this); PowerMockito.mockStatic(TextUtils.class, MeetingListBasicPresenter.class, DateFormat.class, Util.class); meetingProxy = MeetingInfoProxy.newInstance(); meetingProxy.inject(recoverItem); } |
// MeetingInfoProxy.java
// 在MeetingInfoProxy的inject方法中, public void inject(@NonNull ZRCMeetingListItem sourceData) { String st = sourceData.getStartTime(); String et = sourceData.getEndTime(); MeetingState ms = getMeetingState(st, et); } public MeetingState getMeetingState(String startTime, String endTime) { ... try { currentDate.setTime(System.currentTimeMillis()); Date startDateTime = formatter.parse(startTime.replaceAll("Z$", "+00:00")); Date endDateTime = formatter.parse(endTime.replaceAll("Z$", "+00:00")); } catch(Exception e) { ... } ... } |
问题就出在formatter调用parse的时候,程序正常在真机上运行没有任何问题,但是在本地的JVM上运行对应单元测试就会抛异常。
3.问题分析
发现这个问题之后,我的第一反应是没有办法来解决,只好去查。看了几个帖子,没有什么太好的方案,那么只好又重头出发,在报错的地方找找原因。
我查的第一条信息是formatter中的pattern是否与startTime的对应,也就说它能否正确的格式化时间。
private final static String DATE_PATTERN = "yyyy-MM-dd'T'HH:mm:ssZZZ";
看到我的pattern之后,我马上联想到StackoverFlow上的一个solution,我便去Android官方文档去查每个解析token对应的含义。果不其然,问题发生在Z这个token上。我们来看看到底是什么问题:
zToken.png
javaZToken.png
我在Android Platform和Java Platform利用同一段代码分别作了实验:
try { String pattern = "Z"; String tz = "+08:00"; SimpleDateFormat sdf = new SimpleDateFormat(pattern, Locale.getDefault()); Date d = sdf.parse(tz); } catch (ParseException e) { ... } |
测试结果是:在Android平台上运行不会出现问题,在Java平台上运行一定会报错。
这就很奇怪了。接着我去查parse方法的源码,对应Java和Android的版本都看了一遍,果然有收货!在android.jar包中的SimpleDateFormat.java类的subParseNumericZone方法中,有这样一段解释:
Android subParseNumberZone(...)
相信大家看到这里就应该明白了为什么在Android和Java的平台上测试结果不相同。我在下面详尽的解释一下:
问题的起因是TimeZone与pattern的格式不匹配。上文给出的pattern是格式化TimeZone的,内容是Z,这意味着Z可以格式化那些RFC 822 TimeZone的时区字符串。RFC 822 TimeZone的模板是:<i>Sign</i> <i>TwoDigitHours</i> <i>Minutes</i>,例如:“-0800”,
Sign :+ / -
TwoDigitHours:08
Minutes:00
也就是说,严格来说以RFC 822 TimeZone去格式化时区的话,不应该包含中间的冒号(:),所以我们在Java平台上运行上面的那一段代码的时候会抛出ParseException,因为在解析的时候遇到了不符合pattern的字符。而对于Android平台来说,不出错的原因在于SimpleDateFormat.java在android.jar(我使用的是 API 26的jar包)重新修改了subParseNumbericZone方法,虽然Z对应的RFC 822 TimeZone中不允许有冒号出现,但是如果你的TimeZone字符串中仍然有冒号,这也是允许的,并不会抛出异常,解析过程会继续向下执行,并不会break掉,而OpenJDK的版本则会中断解析并返回一个index为0的值,这在上图中展示的注释中得到了验证。而无论是Unit Test还是Java Platform,SimpleDateFormat.java都是来自于OpenJDK的版本,所以这种带冒号的TimeZone字符串是无法完成格式化的。
以上就是问题出现的原因,如果您想了解更加详细的内容,可以去阅读文档和源码。
4.解决方案
解决方案有如下几种:
1.如果你是在写Unit Test,像我给code写单元测试的话,实际的代码找已经指定了pattern就是Z,那么最好的方案就是将测试的TimeZone字符串改写成+0800,去掉中间的冒号,这样就严格遵守了Z的格式化。
2.如果可以修改源码,那么也可以调整Pattern,使用X也可以,因为X代表的是ISO 8601时区标准,它支持的模式更多一些,不过最大的限制就是Android的API了,要求是24+。
上文内容不用于商业目的,如涉及知识产权问题,请权利人联系博为峰小编(021-64471599-8017),我们将立即处理。