Spring ElasticSearch Date

发布时间 2023-06-28 18:19:19作者: 规划中~~~

问题背景

使用spring-data-elasticsearch:4.4.12查询数据,数据映射到对象的时候时间字段格式异常,报错如下

对象和Es通过@document注解进行映射,对象中有一个时间字段

    @Field(type = FieldType.Date, format = {}, pattern = DatePattern.CHINESE_DATE_PATTERN)
    private Date createAt;

通过elasticEearchRestTemplate.save将数据保存到es的时候没有问题,但是通过.search方法读取数据的时候出现时间格式异常的问题

1. 定位问题

通过异常堆栈,定位到报错的源码位置


最后定位到是通过java.time.format.DateTimeFormatter#parse(java.lang.CharSequence, java.time.temporal.TemporalQuery)将es中"xxxx年xx月xx日"解析为java.util.Date是报错的
java.time是jdk1.8加入的时间处理类,同时加入的还有localDate、localDateTime,按照es源码中解析时间的代码,可以简单的将代码说明如下

因为使用了Instant去解析时间,Instant只解析年月日的时间格式会抛出异常,所以考虑这里能不能使用LocalDate去解析,或者将原始时间字符串改成年月日时分秒的格式。
需求决定Es中时间格式不能变,所以修改方案就只剩下了一种,使用LocalDate去解析时间字符串

2. 修改方案

第一种修改方案比较简单,将对象中Date类型字段改成LocalDate即可
第二种修改方案则是考虑修改es的格式转换代码,分析源码后发现
org.springframework.data.elasticsearch.core.convert.ElasticsearchDateConverter被final修饰,所以不能实现子类
org.springframework.data.elasticsearch.core.convert.DatePropertyValueConverter中通过类名ElasticsearchDateConverter定义的属性,无法修噶
但是DatePropertyValueConverter实现了AbstractPropertyValueConverter接口,所以这里考虑重新实现AbstractPropertyValueConverter接口,并且实现该接口只需要实现两个方法,方案可行
最后将重新实现的类作为该方法的返回值即可org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchPersistentProperty#getPropertyValueConverter
具体代码如下

@Slf4j
@Configuration
public class ElasticRestClientConfig {

    @Bean
    public ElasticsearchRestTemplate elasticsearchRestTemplate(RestHighLevelClient restHighLevelClient,
                                                               ElasticsearchConverter elasticsearchConverter) {
        return new ElasticsearchRestTemplate(restHighLevelClient, elasticsearchConverter);
    }

    @Bean
    public ElasticsearchRestTemplate myElasticsearchRestTemplate(RestHighLevelClient restHighLevelClient) {
        SimpleElasticsearchMappingContext simpleElasticsearchMappingContext = new SimpleElasticsearchMappingContext() {
            @Override
            protected ElasticsearchPersistentProperty createPersistentProperty(Property property, SimpleElasticsearchPersistentEntity<?> owner,
                                                                               SimpleTypeHolder simpleTypeHolder) {
                return new SimpleElasticsearchPersistentProperty(property, owner, simpleTypeHolder) {
                    @Override
                    public PropertyValueConverter getPropertyValueConverter() {
                        return new AbstractPropertyValueConverter(this) {
                            @Override
                            public Object write(Object value) {
                                // 该客户端仅用作从es读取数据,所以写方法不需要实现
                                log.error(">>>>>>>>>>>>>>>>>>>>>>>>>> es 写方法未实现");
                                return null;
                            }

                            @Override
                            public Object read(Object value) {
                                return DateUtil.parse(value.toString(), DatePattern.CHINESE_DATE_PATTERN);
                            }
                        };
                    }
                };
            }
        };

        ElasticsearchRestTemplate elasticsearchRestTemplate = new ElasticsearchRestTemplate(restHighLevelClient,
                new MappingElasticsearchConverter(simpleElasticsearchMappingContext));
        log.debug("自定义bean >>>>> {}", elasticsearchRestTemplate);
        return elasticsearchRestTemplate;
    }
}

使用的时候,用@autowire配合@Qualifier("myElasticsearchRestTemplate")指定使用自定义的bean即可

3. 后续

  1. 为什么这里要定义另外一个ElasticsearchRestTemplate?
    因为如果定义了ElasticsearchRestTemplate类型的bean,则jar包中原来的bean会失效,通过org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchDataConfiguration中定义bean是使用的注解
    @ConditionalOnMissingBean(value = ElasticsearchOperations.class, name = "elasticsearchTemplate")我们知道,如果存在ElasticsearchOperations类型或者elasticsearchTemplate名称的bean时,
    jar包原来定义的bean会失效,因为我们自己定义的bean是ElasticsearchRestTemplate类型的,即实现了ElasticsearchOperations接口,所以,如果我们想使用jar中定义的bean,则需要重新定义一遍
  2. 另外一个问题,为什么我们只需要实现read方法,不需要实现write方法?
    因为我们这里只需要定义一个bean,从es中读取数据即可,写入数据我们使用jar包中原来定义的bean即可,所以不存在写的场景,自然不需要实现写的方法