点击上方“Java知音”,选择“置顶公众号”
技术文章第一时间送达!
LocalDate
、
LocalTime
、
LocalDateTime
是Java 8开始提供的时间日期API,主要用来优化Java 8以前对于时间日期的处理操作,确实很方便。笔者在使用的过程中,由于引用LocalDate产生了一个有趣的问题,觉得有必要记录一下。
问题描述
我们系统需要利用原有的核心一个接口报文,向外围提供接口服务。里面有表示日期的字段,原始字段类型定义为String。服务中拿LocalDate.parse来获得LocalDate对象,类似于下面:
LocalDate d = LocalDate.parse("2017/2/21", DateTimeFormatter.ofPattern("yyyy/M/d"));
非常快的开发完了,测试的结果也非常满意。
但是某一天,外围调用我们接口的人反应了一个情况:有一次他们手工做报文,日期写错了,为”2017/2/29”,按照道理我们的服务应该校验日期,然后给调用者返回一个错误,但是实际上什么也没有,正常业务执行了。
我找到这一条的日志,发现后台记录的日期是“2017-02-28”。按照先入为主的概念,2017不是闰年,2月份只有28天,所以应该是校验出错误的。查看代码,发现除了上面的转换,都没有其他对于这个字段的操作,所以一下子就僵住了。
问题查找
由于确实没有其他地方来操作这个字段,百度了一圈,没有任何这方面的提示。实在没有办法,只好追踪源码了。时间日期API在rt.jar中,幸好我们有强大的idea, 直接点开就行:
在LocalDate里面发现,parse调用的是DateTimeFormatter.parse,真正调用的是parseResolved0,有异常就抛出DateTimeParseException:
public T parse(CharSequence text, TemporalQuery query) {
Objects.requireNonNull(text, "text");
Objects.requireNonNull(query, "query");
try {
return parseResolved0(text, null).query(query);
} catch (DateTimeParseException ex) {
throw ex;
} catch (RuntimeException ex) {
throw createError(text, ex);
}
}
经过一连串的debug, 最终定位在java.time.chrono.IsoChronology.resolveYMD方法中,代码为:
@Override // override for performance
LocalDate resolveYMD(Map TemporalField, Long fieldValues, ResolverStyle resolverStyle) {
int y = YEAR.checkValidIntValue(fieldValues.remove(YEAR));
if (resolverStyle == ResolverStyle.LENIENT) {
long months = Math.subtractExact(fieldValues.remove(MONTH_OF_YEAR), 1);
long days = Math.subtractExact(fieldValues.remove(DAY_OF_MONTH), 1);
return LocalDate.of(y, 1, 1).plusMonths(months).plusDays(days);
}
int moy = MONTH_OF_YEAR.checkValidIntValue(fieldValues.remove(MONTH_OF_YEAR));
int dom = DAY_OF_MONTH.checkValidIntValue(fieldValues.remove(DAY_OF_MONTH));
if (resolverStyle == ResolverStyle.SMART) { // previous valid
if (moy == 4 || moy == 6 || moy == 9 || moy == 11) {
dom = Math.min(dom, 30);
} else if (moy == 2) {
dom = Math.min(dom, Month.FEBRUARY.length(Year.isLeap(y)));
}
}
return LocalDate.of(y, moy, dom);
}
可以看到,源码中,对于4、6、9、11月份,她会获取传入的日期天数和30之间的最小值,而对于2月来说,则判断传入的日期天数和28(闰年是29)之间的最小值,这样就能合理的解释了上面为什么”2017/2/29”校验没有错误,直接变成了”2017-2-28”。我又试了一下,把日期改为”2017/2/31”, 也不会有问题,改为“2017/2/32”,就会报异常:
根据上面的情况,推断出:
不大于31, 则根据不同的月份,返回月份的实际天数
其实这种处理说不上好坏,反正我觉得没有什么大问题,只不过和我们业务的要求不太相符。我们保险业务对应日期是要严格校验的,前后一天的日期的变化直接关系到是否能够承保,所以不能容忍这样的“智能”的操作,而是应该抛出异常
解决
问题已经出来了,怎么解决呢?当然有很多解决办法,比如直接甩锅给调用方,理直气壮不带犹豫的,不过这都不是我的风格。还是从源码上改一下吧:理想情况就是如果天数不符合要求,就抛出异常:
LocalDate resolveYMD(Map TemporalField, Long fieldValues, ResolverStyle resolverStyle) {
int y = YEAR.checkValidIntValue(fieldValues.remove(YEAR));
if (resolverStyle == ResolverStyle.LENIENT) {
long months = Math.subtractExact(fieldValues.remove(MONTH_OF_YEAR), 1);
long days = Math.subtractExact(fieldValues.remove(DAY_OF_MONTH), 1);
return LocalDate.of(y, 1, 1).plusMonths(months).plusDays(days);
}
int moy = MONTH_OF_YEAR.checkValidIntValue(fieldValues.remove(MONTH_OF_YEAR));
int dom = DAY_OF_MONTH.checkValidIntValue(fieldValues.remove(DAY_OF_MONTH));
// previous valid
if (resolverStyle == ResolverStyle.SMART) {
if (moy == 4 || moy == 6 || moy == 9 || moy == 11) {
//原来的:dom = Math.min(dom, 30);
//改为:
if (dom 30) {
throw new DateTimeException("The max days of " + moy + " month is 30.");
}
} else if (moy == 2) {
//原来的:dom = Math.min(dom, Month.FEBRUARY.length(Year.isLeap(y)));
//改为:
if (Year.isLeap(y)) {
if (dom 29) {
throw new DateTimeException("The max of days of " + moy + " month is 29.");
}
} else {
if (dom 28) {
throw new DateTimeException("The max days of " + moy + " month is 28.");
}
}
}
}
return LocalDate.of(y, moy, dom);
}
原本在idea里面新建了java.time.chrono.IsoChronology类,把原来的代码拷贝过来,改一下上面的方法就好了,谁知道竟然没有任何变化,这是为什么呢?
原来,java里面有些jar里面的类最优先加载的,你没有办法加载你自己项目中写的同样pagekage和classname的类,怎么办,只好釜底抽薪,先把下面中自己写的类编译,然后在rt.jar中,把IsoChronology.class更改掉,再试成功了:
注意抛出的就是我定义的异常信息。
下面的图是没有替换IsoChronolocy.class的时候返回的,没有异常,只有最后被“智能”返回的信息:
后记
第一次修改jdk源码竟然是从这儿开始的,惭愧的无以复加。
END
Java面试题专栏
我知道你 “在看”
原文始发于微信公众号(Java知音):记一次JDK8中关于LocalDate的一点源码改动