问题背景
使用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
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. 后续
- 为什么这里要定义另外一个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,则需要重新定义一遍 - 另外一个问题,为什么我们只需要实现read方法,不需要实现write方法?
因为我们这里只需要定义一个bean,从es中读取数据即可,写入数据我们使用jar包中原来定义的bean即可,所以不存在写的场景,自然不需要实现写的方法