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

本人花费半年的时间总结的《Java面试指南》已拿腾讯等大厂offer,已开源在github ,欢迎star!

本文GitHub https://github.com/OUYANGSIHAI/JavaInterview 已收录,这是我花了6个月总结的一线大厂Java面试总结,本人已拿大厂offer,欢迎star

原文链接:blog.ouyangsihai.cn >> 记一次JDK8中关于LocalDate的一点源码改动

点击上方“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 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”,就会报异常:

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

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

  • 首先会判断天数是不是大于31,如果大于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, 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更改掉,再试成功了:

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

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

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

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

    后记

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

    END

    Java面试题专栏

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

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

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

    本人花费半年的时间总结的《Java面试指南》已拿腾讯等大厂offer,已开源在github ,欢迎star!

    本文GitHub https://github.com/OUYANGSIHAI/JavaInterview 已收录,这是我花了6个月总结的一线大厂Java面试总结,本人已拿大厂offer,欢迎star

    原文链接:blog.ouyangsihai.cn >> 记一次JDK8中关于LocalDate的一点源码改动


     上一篇
    18个Java8日期处理的实践,太有用了! 18个Java8日期处理的实践,太有用了!
    点击上方“Java知音”,选择“置顶公众号” 技术文章第一时间送达! 作者:胖先森 juejin.im/post/5a795bad6fb9a0634f407ae5 juejin.im/post/5a795
    下一篇 
    面试被问到“零拷贝”!你真的理解吗? 面试被问到“零拷贝”!你真的理解吗?
    点击上方“后端技术精选”,选择“置顶公众号” 技术文章第一时间送达! 作者:ksfzhaohui juejin.im/post/5cad6f1ef265da039f0ef5df juejin.im/post