记一次JDK8中关于LocalDate的一点源码改动

>>最全面的Java面试大纲及答案解析(建议收藏)  

点击上方“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, 直接点开就行:

记一次JDK8中关于LocalDate的一点源码改动

在LocalDate里面发现,parse调用的是DateTimeFormatter.parse,真正调用的是parseResolved0,有异常就抛出DateTimeParseException:

public <T> parse(CharSequence text, TemporalQuery<T> 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, 11).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”,就会报异常:

记一次JDK8中关于LocalDate的一点源码改动


根据上面的情况,推断出:

  • 首先会判断天数是不是大于31,如果大于31,抛出异常,不管是哪个月份

  • 不大于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, 11).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更改掉,再试成功了:

记一次JDK8中关于LocalDate的一点源码改动

注意抛出的就是我定义的异常信息。

下面的图是没有替换IsoChronolocy.class的时候返回的,没有异常,只有最后被“智能”返回的信息:

记一次JDK8中关于LocalDate的一点源码改动


后记

第一次修改jdk源码竟然是从这儿开始的,惭愧的无以复加。

END

Java面试题专栏


【40期】说一下线程池内部工作原理
【39期】Mybatis面试18问,你想知道的都在这里了!
【38期】一份tcp、http面试指南,常考点都给你了
【37期】请你详细说说类加载流程,类加载机制及自定义类加载器
【36期】说说 如何停止一个正在运行的线程?
【35期】谈谈你对Java线程之间通信方式的理解
【34期】谈谈为什么要拆分数据库?有哪些方法?
【33期】分别谈谈联合索引生效和失效的条件
【32期】你知道Redis的字符串是怎么实现的吗?
【31期】了解什么是 redis 的雪崩、穿透和击穿?redis 崩溃之后会怎么样?应对措施是什么



记一次JDK8中关于LocalDate的一点源码改动

我知道你 “在看记一次JDK8中关于LocalDate的一点源码改动

原文始发于微信公众号(Java知音):记一次JDK8中关于LocalDate的一点源码改动