【问题标题】:How to format date correctly using Spring Data Elasticsearch如何使用 Spring Data Elasticsearch 正确格式化日期
【发布时间】:2020-04-03 09:32:26
【问题描述】:

我正在使用 SpringBoot 2.2.5 和 Elasticsearch 6.8.6。我正在从 Spring Data Jest 迁移到使用带有 ElasticsearchEntityMapper 的 Spring Data Elasticsearch REST 传输机制。

我有一个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 配置什么吗?

【问题讨论】:

  • 您的日期字段映射如何?还是您使用动态映射,这意味着您之前没有为索引设置显式映射?
  • 使用本站unixtimestamp.com将错误信息中指出的unix时间戳转换为 52225-05-05T20:47:46+00:00 。所以你的输入数据有问题。
  • 我依赖动态映射。
  • 测试我正在使用trend1.setDate(new Date()); 不知道为什么会失败。
  • 没有@JsonFormat注解可以再试一次吗?

标签: elasticsearch date-format spring-data-elasticsearch


【解决方案1】:

首先,请不要使用基于 Jackson 的默认映射器。它在 Spring Data Elasticsearch (4.0) 的下一个主要版本中被删除。然后将没有可用的选择,并且在内部使用ElasticsearchEntityMapper

关于你的问题:Spring Boot目前使用的3.2版中的ElasticsearchEntityMapper不使用@Field属性中的日期相关信息来转换实体,它仅用于索引映射创建。这是一个缺失的功能或错误,并在下一个主要版本中修复,整个映射过程在那里进行了大修。

在当前情况下您可以做什么:您需要添加自定义转换器。您可以像这样在配置类中执行此操作:

@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 引入的时间类,如 LocalDateLocalDateTime,Spring Data Elasticsearch 支持这些开箱即用,而 java.util.Date 需要自定义转换器。

Edit 09.04.2020:添加了必要的@WritingConverter@ReadingConverter 注释。

编辑 19.04.2020:Spring Data Elasticsearch 4.0 将支持开箱即用的 java.util.Date 类以及 @Field 注释。

【讨论】:

  • 感谢 P.J,非常感谢您的支持。我设法让Date 与客户转换器合作。然后我尝试在没有自定义转换器的情况下使用 LocalDateTime 并得到相同的异常。我一定做错了什么。您可以发布如何使用LocalDateTimeZonedDateTime 的示例吗?也许这应该放入当前的文档中,因为其他开发人员也可能面临同样的问题。
  • 在 3.2.5 中,您还需要 CustomConverters,它在 Spring Data Elasticsearch 中,java.time 类开箱即用,只需 `@Field' 注释
  • 好的,谢谢。今天刚回到代码,并尝试在上面启用日期转换器的实体中添加更多 String 属性。保存到 Elasticsearch 时,字符串属性都设置为 null。转换器不应该只影响日期字段吗?
  • 当然,这就是他们有源和目标类型的原因。你有新属性的 getter 和 setter 吗?
  • 是的。如果我禁用转换器,实体会被正确写入(禁止日期)。 Elasticsearch 实体是使用来自另一个实体(另一个数据存储)的模型映射器创建的,我认为这不会导致问题。
【解决方案2】:

由于我是新加入者,我无法根据堆栈规则在 @P.J.Meisch 的 anwser 下发表评论。 我也遇到了这个问题,并用@P.J.Meisch's anwser 解决了这个问题。 但只需使用@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,而不是我。

【讨论】:

    猜你喜欢
    • 2016-05-10
    • 2022-01-06
    • 2015-11-09
    • 1970-01-01
    • 1970-01-01
    • 2012-02-15
    • 2021-03-03
    • 1970-01-01
    • 2021-06-05
    相关资源
    最近更新 更多