问题描述
系统一开始使用es没有使用那个@Document 实体来建立索引mapping,存储和更新也使用的map对象进行的。所以一开始没啥问题,后面某个业务需要固定mapping时,就尝试使用了那个Document实体。结果在查询时出现了java.time.DateTimeException: Unable to obtain Instant from TemporalAccessor
的错误。
排查
根据堆栈信息问题定位在at java.time.Instant.from(Instant.java:378) org.springframework.data.elasticsearch.core.convert.ElasticsearchDateConverter.parse(ElasticsearchDateConverter.java:125)
,肯定和文档类的日期字段有关系了,文档存入没问题,那个Date字段是@Field(type = FieldType.Date, format = DateFormat.date)
格式是yyyy-MM-dd
,打断点看一下为啥Instant.from要报错,结果不太清楚为啥会报错,断点执行评估表达式时我手动new 个完整Date格式化的字符串没问题,估计是Instant.from的参数有问题。
解决
最后发现是spring data elasticsearch处理日期写入和读出的代码不一致。
是个bug, 参考来源:https://segmentfault.com/q/1010000023765242
首先现在我们理清spring data elasticsearch处理过程大体为:
写入:Date -> Instant(TemporalAccessor) -> String
读出:String -> Parese(TemporalAccessor) -> Instant(TemporalAccessor) -> Date
不过再一次仔细看完写入的代码发现,其实Instant(TemporalAccessor)不是直接到String
@Override
public String format(TemporalAccessor accessor){
return printer.format(DateFormatters.from(accessor));
}
Instant(TemporalAccessor)还经历了一次DateFormatters.from,
该方法是把一个TemporalAccessor转换为ZonedDateTime,再由ZonedDateTime转换成String,此时写入过程变为
写入:Date -> Instant(TemporalAccessor) -> ZonedDateTime(TemporalAccessor) -> String
这下也就释然了,为啥呢?因为java.util.Date在功能上就是和新的java8时间API的ZonedDateTime差不多的,这样的转换也是合理的。
反过来看读出时并没有这一步操作啊,其实这就不合理了,为啥呢?比如es里存的时间格式恰好没有时分秒,只有年月日,那它该怎么从一个年月日转换为Date呢?必须要补齐时分秒啊,这一步操作没有的话,是不能把TemporalAccessor转化为一个Instant的,Instant可是一个时间戳
所以正确的读出的过程应该是这样:
String -> Parese(TemporalAccessor) -> ZonedDateTime(TemporalAccessor) -> Instant(TemporalAccessor) -> Date
所以如果我们能把ElasticsearchDateConverter.parse的处理改为下面这样就可以了
public Date parse(String input){
return new Date(Instant.from(DateFormatters.from(dateFormatter.parse(input))).toEpochMilli());
}
以上为网上找到的问题解析,原文中采用替换SimpleElasticsearchMappingContext Bean来暂时修复该问题,但是时过境迁,现在早就应该修复了。所以我采用更新依赖的方式解决,项目使用的elasticsearch为7.6,对应springboot为2.3.x, 直接使用最后2.3最后一个版本,修改spring-boot-dependencies版本为2.3.12.RELEASE,对应的spring-boot-starter-data-elasticsearch也会使用对应的最后的一个版本。
Q.E.D.