如何使用 Spring Data Elasticsearch 正确格式化日期
How to format date correctly using Spring Data Elasticsearch
我正在使用 SpringBoot 2.2.5 和 Elasticsearch 6.8.6。我正在从 Spring Data Jest 迁移到使用 Spring Data Elasticsearch REST 传输机制 ElasticsearchEntityMapper
.
我有一个 Date
字段,其定义如下:
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSZ")
@Field(type = FieldType.Date, format = DateFormat.custom, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSZ")
private Date date;
我想像这样在 Elasticsearch 中存储日期:
"date": "2020-04-02T14:49:05.672+0000"
当我启动应用程序时,索引已创建,但当我尝试保存实体时出现以下异常:
Caused by: org.elasticsearch.client.ResponseException: method [POST], host [http://localhost:9200], URI [/trends/estrend?timeout=1m], status line [HTTP/1.1 400 Bad Request]
{"error":{"root_cause":[{"type":"mapper_parsing_exception","reason":"failed to parse field [date] of type [date] in document with id 'rS5UP3EB9eKtCTMXW_Ky'"}],"type":"mapper_parsing_exception","reason":"failed to parse field [date] of type [date] in document with id 'rS5UP3EB9eKtCTMXW_Ky'","caused_by":{"type":"illegal_argument_exception","reason":"Invalid format: \"1585905425266\" is malformed at \"5266\""}},"status":400}
关于我做错了什么以及我应该如何解决它的任何指示?
配置和实体定义如下:
@Configuration
public class ElasticsearchConfig extends AbstractElasticsearchConfiguration {
@Value("${spring.data.elasticsearch.host}")
private String elasticSearchHost;
@Value("${spring.data.elasticsearch.port}")
private String elasticSearchPort;
@Bean
public RestHighLevelClient elasticsearchClient() {
final ClientConfiguration clientConfiguration = ClientConfiguration.builder()
.connectedTo(elasticSearchHost + ":" + elasticSearchPort)
.usingSsl()
.build();
return RestClients.create(clientConfiguration).rest();
}
@Bean
public EntityMapper entityMapper() {
ElasticsearchEntityMapper entityMapper = new ElasticsearchEntityMapper(elasticsearchMappingContext(), new DefaultConversionService());
entityMapper.setConversions(elasticsearchCustomConversions());
return entityMapper;
}
}
package com.es.test;
import java.util.Date;
import java.util.UUID;
import com.fasterxml.jackson.annotation.JsonFormat;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.DateFormat;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
@Document(indexName = "trends")
public class EsTrend {
@Id
private UUID id;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSZ")
@Field(type = FieldType.Date, format = DateFormat.custom, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSZ")
private Date date;
private String entityOrRelationshipId;
// getter and setters
}
更新:
如果我禁用 ElasticsearchEntityMapper
bean,我不会得到异常并且日期以正确的格式写入 Elasticsearch。我还需要为 ElasticsearchEntityMapper
配置什么吗?
首先,请不要使用基于 Jackson 的默认映射器。它在 Spring Data Elasticsearch (4.0) 的下一个主要版本中被删除。那么就没有选择可用了,内部使用ElasticsearchEntityMapper
关于你的问题:目前Spring Boot使用的3.2版本的ElasticsearchEntityMapper
没有使用@Field
属性中的日期相关信息来转换实体,它仅用于创建索引映射。这是一个缺失的功能或错误,并在下一个主要版本中修复,整个映射过程在那里进行了大修。
您目前的情况可以做什么:您需要添加自定义转换器。您可以在您的配置 class 中这样做:
@Configuration
public class ElasticsearchConfig extends AbstractElasticsearchConfiguration {
private static SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
@Value("${spring.data.elasticsearch.host}")
private String elasticSearchHost;
@Value("${spring.data.elasticsearch.port}")
private String elasticSearchPort;
@Bean
public RestHighLevelClient elasticsearchClient() {
final ClientConfiguration clientConfiguration = ClientConfiguration.builder()
.connectedTo(elasticSearchHost + ":" + elasticSearchPort)
.usingSsl()
.build();
return RestClients.create(clientConfiguration).rest();
}
@Bean
public EntityMapper entityMapper() {
ElasticsearchEntityMapper entityMapper = new ElasticsearchEntityMapper(elasticsearchMappingContext(), new DefaultConversionService());
entityMapper.setConversions(elasticsearchCustomConversions());
return entityMapper;
}
@Override
public ElasticsearchCustomConversions elasticsearchCustomConversions() {
return new ElasticsearchCustomConversions(Arrays.asList(DateToStringConverter.INSTANCE, StringToDateConverter.INSTANCE));
}
@WritingConverter
enum DateToStringConverter implements Converter<Date, String> {
INSTANCE;
@Override
public String convert(Date date) {
return formatter.format(date);
}
}
@ReadingConverter
enum StringToDateConverter implements Converter<String, Date> {
INSTANCE;
@Override
public Date convert(String s) {
try {
return formatter.parse(s);
} catch (ParseException e) {
return null;
}
}
}
}
您仍然需要在 @Field
注释中包含日期格式,因为创建正确的索引映射需要它。
并且您应该更改您的代码以使用 Java 8 引入时间 class 像 LocalDate
或 LocalDateTime
、Spring 数据 Elasticsearch 支持这些开箱即用,而 java.util.Date
需要自定义转换器。
编辑 09.04.2020: 添加了必要的 @WritingConverter
和 @ReadingConverter
注释。
编辑 2020 年 4 月 19 日: Spring Data Elasticsearch 4.0 将支持开箱即用的 java.util.Date
class @Field
注释也是如此。
由于我是新人,根据堆栈规则,我不能在@P.J.Meisch的回答下发表评论。
我也遇到了这个问题,用@P.J.Meisch的答案解决了。
但是@ReadingConverter 只需稍作改动。
实际上,从 ES 读取的原始类型是 Long,而我们需要的 java 中的结果类型是 LocalDateTime。因此,读取转换器应该是 Long 到 LocalDateTime。
代码如下:
@Configuration
public class ElasticsearchClientConfig extends AbstractElasticsearchConfiguration {
public final static int TIME_OUT_MILLIS = 50000;
@Autowired
private ElasticsearchProperties elasticsearchProperties;
@Override
@Bean
public RestHighLevelClient elasticsearchClient() {
final ClientConfiguration clientConfiguration = ClientConfiguration.builder()
.connectedTo(elasticsearchProperties.getHost() + ":" + elasticsearchProperties.getPort())
.withBasicAuth(elasticsearchProperties.getName(), elasticsearchProperties.getPassword())
.withSocketTimeout(TIME_OUT_MILLIS)
.withConnectTimeout(TIME_OUT_MILLIS)
.build();
return RestClients.create(clientConfiguration).rest();
}
/**
* Java LocalDateTime to ElasticSearch Date mapping
*
* @return EntityMapper
*/
@Override
@Bean
public EntityMapper entityMapper() {
ElasticsearchEntityMapper entityMapper = new ElasticsearchEntityMapper(elasticsearchMappingContext(), new DefaultConversionService());
entityMapper.setConversions(elasticsearchCustomConversions());
return entityMapper;
}
@Override
public ElasticsearchCustomConversions elasticsearchCustomConversions() {
return new ElasticsearchCustomConversions(Arrays.asList(DateToStringConverter.INSTANCE, LongToLocalDateTimeConverter.INSTANCE));
}
@WritingConverter
enum DateToStringConverter implements Converter<Date, String> {
/**
* instance
*/
INSTANCE;
@Override
public String convert(@NonNull Date date) {
return DateUtil.format(date, DateConstant.TIME_PATTERN);
}
}
**@ReadingConverter
enum LongToLocalDateTimeConverter implements Converter<Long, LocalDateTime> {
/**
* instance
*/
INSTANCE;
@Override
public LocalDateTime convert(@NonNull Long s) {
return LocalDateTime.ofInstant(Instant.ofEpochMilli(s), ZoneId.systemDefault());
}
}**
}
和 DateUtil 文件:
public class DateUtil {
/**
* lock obj
*/
private static final Object LOCK_OBJ = new Object();
/**
* sdf Map for different pattern
*/
private static final Map<String, ThreadLocal<SimpleDateFormat>> LOCAL_MAP = new HashMap<>();
/**
* thread safe
*
* @param pattern pattern
* @return SimpleDateFormat
*/
private static SimpleDateFormat getSdf(final String pattern) {
ThreadLocal<SimpleDateFormat> tl = LOCAL_MAP.get(pattern);
if (tl == null) {
synchronized (LOCK_OBJ) {
tl = LOCAL_MAP.get(pattern);
if (tl == null) {
System.out.println("put new sdf of pattern " + pattern + " to map");
tl = ThreadLocal.withInitial(() -> {
System.out.println("thread: " + Thread.currentThread() + " init pattern: " + pattern);
return new SimpleDateFormat(pattern);
});
LOCAL_MAP.put(pattern, tl);
}
}
}
return tl.get();
}
/**
* format
*
* @param date date
* @param pattern pattern
* @return String
*/
public static String format(Date date, String pattern) {
return getSdf(pattern).format(date);
}
}
最后,
请投票给@P.J.Meisch,而不是我。
我正在使用 SpringBoot 2.2.5 和 Elasticsearch 6.8.6。我正在从 Spring Data Jest 迁移到使用 Spring Data Elasticsearch REST 传输机制 ElasticsearchEntityMapper
.
我有一个 Date
字段,其定义如下:
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSZ")
@Field(type = FieldType.Date, format = DateFormat.custom, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSZ")
private Date date;
我想像这样在 Elasticsearch 中存储日期:
"date": "2020-04-02T14:49:05.672+0000"
当我启动应用程序时,索引已创建,但当我尝试保存实体时出现以下异常:
Caused by: org.elasticsearch.client.ResponseException: method [POST], host [http://localhost:9200], URI [/trends/estrend?timeout=1m], status line [HTTP/1.1 400 Bad Request]
{"error":{"root_cause":[{"type":"mapper_parsing_exception","reason":"failed to parse field [date] of type [date] in document with id 'rS5UP3EB9eKtCTMXW_Ky'"}],"type":"mapper_parsing_exception","reason":"failed to parse field [date] of type [date] in document with id 'rS5UP3EB9eKtCTMXW_Ky'","caused_by":{"type":"illegal_argument_exception","reason":"Invalid format: \"1585905425266\" is malformed at \"5266\""}},"status":400}
关于我做错了什么以及我应该如何解决它的任何指示?
配置和实体定义如下:
@Configuration
public class ElasticsearchConfig extends AbstractElasticsearchConfiguration {
@Value("${spring.data.elasticsearch.host}")
private String elasticSearchHost;
@Value("${spring.data.elasticsearch.port}")
private String elasticSearchPort;
@Bean
public RestHighLevelClient elasticsearchClient() {
final ClientConfiguration clientConfiguration = ClientConfiguration.builder()
.connectedTo(elasticSearchHost + ":" + elasticSearchPort)
.usingSsl()
.build();
return RestClients.create(clientConfiguration).rest();
}
@Bean
public EntityMapper entityMapper() {
ElasticsearchEntityMapper entityMapper = new ElasticsearchEntityMapper(elasticsearchMappingContext(), new DefaultConversionService());
entityMapper.setConversions(elasticsearchCustomConversions());
return entityMapper;
}
}
package com.es.test;
import java.util.Date;
import java.util.UUID;
import com.fasterxml.jackson.annotation.JsonFormat;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.DateFormat;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
@Document(indexName = "trends")
public class EsTrend {
@Id
private UUID id;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSZ")
@Field(type = FieldType.Date, format = DateFormat.custom, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSZ")
private Date date;
private String entityOrRelationshipId;
// getter and setters
}
更新:
如果我禁用 ElasticsearchEntityMapper
bean,我不会得到异常并且日期以正确的格式写入 Elasticsearch。我还需要为 ElasticsearchEntityMapper
配置什么吗?
首先,请不要使用基于 Jackson 的默认映射器。它在 Spring Data Elasticsearch (4.0) 的下一个主要版本中被删除。那么就没有选择可用了,内部使用ElasticsearchEntityMapper
关于你的问题:目前Spring Boot使用的3.2版本的ElasticsearchEntityMapper
没有使用@Field
属性中的日期相关信息来转换实体,它仅用于创建索引映射。这是一个缺失的功能或错误,并在下一个主要版本中修复,整个映射过程在那里进行了大修。
您目前的情况可以做什么:您需要添加自定义转换器。您可以在您的配置 class 中这样做:
@Configuration
public class ElasticsearchConfig extends AbstractElasticsearchConfiguration {
private static SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
@Value("${spring.data.elasticsearch.host}")
private String elasticSearchHost;
@Value("${spring.data.elasticsearch.port}")
private String elasticSearchPort;
@Bean
public RestHighLevelClient elasticsearchClient() {
final ClientConfiguration clientConfiguration = ClientConfiguration.builder()
.connectedTo(elasticSearchHost + ":" + elasticSearchPort)
.usingSsl()
.build();
return RestClients.create(clientConfiguration).rest();
}
@Bean
public EntityMapper entityMapper() {
ElasticsearchEntityMapper entityMapper = new ElasticsearchEntityMapper(elasticsearchMappingContext(), new DefaultConversionService());
entityMapper.setConversions(elasticsearchCustomConversions());
return entityMapper;
}
@Override
public ElasticsearchCustomConversions elasticsearchCustomConversions() {
return new ElasticsearchCustomConversions(Arrays.asList(DateToStringConverter.INSTANCE, StringToDateConverter.INSTANCE));
}
@WritingConverter
enum DateToStringConverter implements Converter<Date, String> {
INSTANCE;
@Override
public String convert(Date date) {
return formatter.format(date);
}
}
@ReadingConverter
enum StringToDateConverter implements Converter<String, Date> {
INSTANCE;
@Override
public Date convert(String s) {
try {
return formatter.parse(s);
} catch (ParseException e) {
return null;
}
}
}
}
您仍然需要在 @Field
注释中包含日期格式,因为创建正确的索引映射需要它。
并且您应该更改您的代码以使用 Java 8 引入时间 class 像 LocalDate
或 LocalDateTime
、Spring 数据 Elasticsearch 支持这些开箱即用,而 java.util.Date
需要自定义转换器。
编辑 09.04.2020: 添加了必要的 @WritingConverter
和 @ReadingConverter
注释。
编辑 2020 年 4 月 19 日: Spring Data Elasticsearch 4.0 将支持开箱即用的 java.util.Date
class @Field
注释也是如此。
由于我是新人,根据堆栈规则,我不能在@P.J.Meisch的回答下发表评论。 我也遇到了这个问题,用@P.J.Meisch的答案解决了。 但是@ReadingConverter 只需稍作改动。 实际上,从 ES 读取的原始类型是 Long,而我们需要的 java 中的结果类型是 LocalDateTime。因此,读取转换器应该是 Long 到 LocalDateTime。 代码如下:
@Configuration
public class ElasticsearchClientConfig extends AbstractElasticsearchConfiguration {
public final static int TIME_OUT_MILLIS = 50000;
@Autowired
private ElasticsearchProperties elasticsearchProperties;
@Override
@Bean
public RestHighLevelClient elasticsearchClient() {
final ClientConfiguration clientConfiguration = ClientConfiguration.builder()
.connectedTo(elasticsearchProperties.getHost() + ":" + elasticsearchProperties.getPort())
.withBasicAuth(elasticsearchProperties.getName(), elasticsearchProperties.getPassword())
.withSocketTimeout(TIME_OUT_MILLIS)
.withConnectTimeout(TIME_OUT_MILLIS)
.build();
return RestClients.create(clientConfiguration).rest();
}
/**
* Java LocalDateTime to ElasticSearch Date mapping
*
* @return EntityMapper
*/
@Override
@Bean
public EntityMapper entityMapper() {
ElasticsearchEntityMapper entityMapper = new ElasticsearchEntityMapper(elasticsearchMappingContext(), new DefaultConversionService());
entityMapper.setConversions(elasticsearchCustomConversions());
return entityMapper;
}
@Override
public ElasticsearchCustomConversions elasticsearchCustomConversions() {
return new ElasticsearchCustomConversions(Arrays.asList(DateToStringConverter.INSTANCE, LongToLocalDateTimeConverter.INSTANCE));
}
@WritingConverter
enum DateToStringConverter implements Converter<Date, String> {
/**
* instance
*/
INSTANCE;
@Override
public String convert(@NonNull Date date) {
return DateUtil.format(date, DateConstant.TIME_PATTERN);
}
}
**@ReadingConverter
enum LongToLocalDateTimeConverter implements Converter<Long, LocalDateTime> {
/**
* instance
*/
INSTANCE;
@Override
public LocalDateTime convert(@NonNull Long s) {
return LocalDateTime.ofInstant(Instant.ofEpochMilli(s), ZoneId.systemDefault());
}
}**
}
和 DateUtil 文件:
public class DateUtil {
/**
* lock obj
*/
private static final Object LOCK_OBJ = new Object();
/**
* sdf Map for different pattern
*/
private static final Map<String, ThreadLocal<SimpleDateFormat>> LOCAL_MAP = new HashMap<>();
/**
* thread safe
*
* @param pattern pattern
* @return SimpleDateFormat
*/
private static SimpleDateFormat getSdf(final String pattern) {
ThreadLocal<SimpleDateFormat> tl = LOCAL_MAP.get(pattern);
if (tl == null) {
synchronized (LOCK_OBJ) {
tl = LOCAL_MAP.get(pattern);
if (tl == null) {
System.out.println("put new sdf of pattern " + pattern + " to map");
tl = ThreadLocal.withInitial(() -> {
System.out.println("thread: " + Thread.currentThread() + " init pattern: " + pattern);
return new SimpleDateFormat(pattern);
});
LOCAL_MAP.put(pattern, tl);
}
}
}
return tl.get();
}
/**
* format
*
* @param date date
* @param pattern pattern
* @return String
*/
public static String format(Date date, String pattern) {
return getSdf(pattern).format(date);
}
}
最后,
请投票给@P.J.Meisch,而不是我。