事情是这样的
在不久的以前,我们项目的 Tech Lead 决定在git repo中引入 DependaBot 来对项目中的依赖做检查并升级。我们的一个使用 SpringBoot 的服务也就这样成了待升级依赖的一份子。我们待升级的依赖包括但不限于:
- Bump newrelic-agent from 5.8.0 to 6.3.0 …
- Bump guava from 28.0-jre to 30.1-jre …
- Bump spring-hateoas from 1.1.0.RELEASE to 1.2.3 …
- Bump postgresql from 42.2.8 to 42.2.18 …
- Bump cloudwatch from 2.13.41 to 2.15.66 …
- Bump json-schema-validator from 4.2.0 to 4.3.3 …
- Bump org.springframework.boot from 2.2.5.RELEASE to 2.4.2 …
- Bump io.spring.dependency-management …
- Bump io.freefair.lombok from 4.1.3 to 5.3.0 …
- Bump org.flywaydb.flyway from 6.1.3 to 7.5.0 …
可以看到,几乎都将这些依赖升级到了最新的版本,甚至 SpringBoot2.4.2 是在这次升级的前三天 release 的。但是我们不慌,升级依赖什么的对我们来说跟喝水一样简单,因为…
1 | jacocoTestCoverageVerification { |
我们的代码的测试覆盖率的要求是惊人的100%🤣 这在我之前的公司是绝对无法实现的。不仅仅是 unit test, 我们还有 integration 测试覆盖,还有用到 cypress 又一次覆盖了所有的 endpoint。不就是改改代码么/升级依赖啥的么,随便玩。
于是
梭哈!👨🏻💻👨🏻💻👨🏻💻👨🏻💻👨🏻💻👨🏻💻👨🏻💻升级,跑测试!
几分钟后:
行嘛,不出我所料(才怪🙃)果然挂了。
打开log一看, emmm…
1 | Resolved [org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot deserialize value of type `java.time.LocalDate` from String "2020-01-15": Failed to deserialize java.time.LocalDate: (java.time.format.DateTimeParseException) Text '2020-01-15' could not be parsed: Unable to obtain LocalDate from TemporalAccessor: {YearOfEra=2020, MonthOfYear=1, DayOfMonth=15},ISO of type java.time.format.Parsed; nested exception is com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type `java.time.LocalDate` from String "2020-01-15": Failed to deserialize java.time.LocalDate: (java.time.format.DateTimeParseException) Text '2020-01-15' could not be parsed: Unable to obtain LocalDate from TemporalAccessor: {YearOfEra=2020, MonthOfYear=1, DayOfMonth=15},ISO of type java.time.format.Parsed |
汪的发!?😢 用的好好的@JsonFormat
怎么就突然不好使了?
问题排查
打开代码看一下:
我有这么一个对象:
1 | public class Demo { |
其中配置了 localDate 的反序列化为严格模式lenient = OptBoolean.FALSE
,防止将 number 反序列化为日期,那样是不正确的。
有这么一个 controller:
1 |
|
代码很简单,就是有一个对象,接收一个 LocalDate 的属性,用pattern yyyy-MM-dd
接收类似于2020-01-15
这样格式的日期。
但是之前用得好好的升级了 SpringBoot2.4.2之后却用不了了?emmmm… 一定是 SpringBoot 升级升了啥不该升的玩应,🧐我要去 SpringBoot 的升级日志里看看,是不是升级了 Jackson 啥的,万一找到一个大霸哥🦟,提个 PR 不就从此成为顶级开源项目的 contributor 了。。。😎
SpringBoot 2.4.2 升级日志
去 GitHub 上打开 SpringBoot Release v2.4.2 , 浏览下 Bug Fixes 、 Documentation、Dependency Upgrades, 发现一行:
Upgrade to Jackson Bom 2.11.4 #24726
果然,升级了 Jackson 到2.11.4
。 对比了一下发现我原先的 SpringBoot 中的 Jackson 版本是2.10.2
, emm… 一般这种稍大的版本升级都伴随着很多 magic 的事情。总之接下来要去 Jackson 的升级日志里面看一下,有什么升级跨越了2.10.*
和2.11.*
这两个版本。
Jackson 2.11升级日志
这个升级日志在它 GitHub 的 wiki 里,点击Jackson Release 2.11。
阅读一下,第一遍竟然没有找到任何线索,阿西吧🥵,通篇与@JsonFormat
的字眼几乎没有。但是,功夫不负有心人,由于我这个错误是时间类型的转换问题,在如下所示的更改中,发现对于Java 8date/time
有相关升级:
- #148: Allow strict
LocalDate
parsing
打开这个 issue 看一下,如他们所讨论的,在之前配置了@JsonFormat(pattern = "yyyy-MM-dd", lenient = OptBoolean.FALSE)
, Jackson 创建的DateTimeFormatter
还是会使用ResolverStyle.SMART
smart 模式,并不能阻止非法日期2019-11-31
的输入。 所以在2.11
版本之后, 如果设置了lenient = OptBoolean.FALSE
, DateTimeFormatter
会使用严格模式,看看代码:
在Jackson 中的JSR310DateTimeDeserializerBase
这个类中,有这么一个方法createContextual
, 有这么一段代码:
1 | if (!deser.isLenient()) { |
可是,为什么DateTimeFormatter
使用了严格模式,会导致上述报错呢?
Java8 之后的 java.time 之 DateTimeFormatter
严格模式下的字符串转LocalDate
举个🌰👀👀
1 | public static void main(String[] args) { |
执行,并抛出异常,转换失败!
1 | Exception in thread "main" java.time.format.DateTimeParseException: Text '2021-01-20' could not be parsed: Unable to obtain LocalDate from TemporalAccessor: {YearOfEra=2021, DayOfMonth=20, MonthOfYear=1},ISO of type java.time.format.Parsed |
关键字YearOfEra
?🧐啊,带年代的年?沃德发😱?
打开类DateTimeFormatter
搜索一下yyyy
,发现一段注释里面y: year-of-era
:
1 | * All letters 'A' to 'Z' and 'a' to 'z' are reserved as pattern letters. The |
原来,u
才是代表年的那个字母,而y
是指带有纪元(era)的年,在DateTimeFormatter
严格模式下使用,yyyy-MM-dd
并不合法,正确的使用姿势是uuuu-MM-dd
!!!
所以yyyy
要怎么用呢?如下,带上G
表示一下公元前或者公元后吧。AD/BC
1 | DateTimeFormatter formatter = DateTimeFormatter |
至此,大功告成,问题解决,依赖也成功升级
总结一下
问题解决了心情很好,但是反思一下,Java8 都出来这么久了,新的日期时间也用了很多,但是就是忽略了y
和u
这么不起眼的小问题!
在问题的排查中,实际上并不如上述流程这样顺利,我还在 Jackson 的 GitHub 里面提了 issue
https://github.com/FasterXML/jackson-modules-java8/issues/199
在我排查 Jackson 的源码的时候,发现他们对于这段代码df = df.withResolverStyle(ResolverStyle.STRICT);
的升级,并没有很完善的测试。在他们的源码中可以看到test case 都是只是测试了异常情况,并没有覆盖原先本应该正确的 case(可见 unit test 是多么的重要),他们的测试源码如下:
1 | public class LocalDateDeserTest extends ModuleTestBase { |
这个测试的问题在于,将{ "value" : "2019-11-31"}
改成合法的也能跑过。因为严格模式下, yyyy-MM-dd
并不合法,同样会跑出InvalidFormatException
异常。所以我在 Jackson 的jackson-modules-java8
这个 repo 下还提了一个 PR 去修改他们的测试用例:
https://github.com/FasterXML/jackson-modules-java8/pull/201
不过也只是简单覆盖一下这个 case,对于其他用到yyyy
的测试并未做修改,希望我的 PR 能被合进去吧哈哈😜虽然只是单元测试并不是代码功能,但也很有用啊。