java springboot整合elasticsearch时关于LocalDateTime处理的方式

发布时间 2023-06-06 15:23:53作者: lythen

环境:

springboot version:2.7.2

spring-data-elasticsearch: 2.7.2

elaseicsearch: 7.10.1

以上是我测试环境,如果环境相差太大,可能会有所差异,仅做参考。

写博客主要是为了记录今天对LocalDateTime处理的过程。

首先,简单介绍一下业务环境。我们的业务库有大量数据,需要导到Elasticsearch中,以使用Elasticsearch的全文索。但有个问题,就是在业务库的时间,是2023-06-06 12:32:37.224这种模式的,并且,业务库的实体时间属性使用的都是LocalDateTime,如果直接录入到ES中,会发现,时间多了8小时。其实原因我也知道,所以这里不探究原因,只做处理。

在做处理之前,先简单了解下LocalDateTime,如下图,可以看到,LocalDateTime相当简单,关于LocalDateTime的知识,大家可以参考LocalDateTime - 廖雪峰的官方网站 (liaoxuefeng.com),因为LocalDateTime无时区的概念,如果仅是“2023-06-06 12:32:37.224”会被默认为0时区的时间录进去,而中国是东八区,比0时区早8小时。这样,自然就产生了时间差,多了了8小时。

那??直接把时间减去8小时,再录进去,不就行了吗?这样也对,比较暴力。当然,处理方法不只一种,我只说我的方式。

再次,我们来看ES里的时间,最后录入的时间,和原来的时间相比,多了个T和Z。

{
"fields": {
    "@timestamp": [
      "2023-06-06T04:32:37.224Z"
    ],
    "ywblStartTime": [
      "2023-06-06T04:32:37.224Z"
    ],
    "ywblEndTime": [
      "2023-06-06T04:32:37.224Z"
    ]
  }
}

要想处理时间,得先了解这个T和Z是什么意思。T可以忽略,就是个分隔符,看成空格即可。关键在于Z这个,Z表示的是UTC,即国际标准时间,可以理解为0时区的时间。回到上面,如果我们在Mysql中的数据库的时间是2023-06-06 12:32:37.224,那对应的国际时间就是2023-06-06T04:32:37.224Z。明显发现,标准时间表示中,是少8小时的。这也是我这次的目标时间。

因此,要做的处理就有两个,一个是时间录入到ES时,需要从LocalDateTime转换成国际标准时间,查询ES时,需要把ES的时间转回LocalDateTime。

通过查看包org.springframework.data.elasticsearch.annotations,看到一个切面类ValueConverter,需要一个PropertyValueConverter参数,如下,没跑了,就是你了!

@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD, ElementType.ANNOTATION_TYPE })
@Documented
@Inherited
public @interface ValueConverter {

    /**
     * Defines the class implementing the {@link PropertyValueConverter} interface. If this is a normal class, it must
     * provide a default constructor with no arguments. If this is an enum and thus implementing a singleton by enum it
     * must only have one enum value.
     * 
     * @return the class to use for conversion
     */
    Class<? extends PropertyValueConverter> value();
}

下面是接口类PropertyValueConverter,其实也相当简单,只要实现这两个方法即可。英文可以看看,其中write是从实体到ES的转换,这里要返回的是String ,read则是从ES于实体的转换,返回的是LocalDateTime

public interface PropertyValueConverter {

    /**
     * Converts a property value to an elasticsearch value. If the converter cannot convert the value, it must return a
     * String representation.
     *
     * @param value the value to convert, must not be {@literal null}
     * @return The elasticsearch property value, must not be {@literal null}
     */
    Object write(Object value);

    /**
     * Converts an elasticsearch property value to a property value.
     *
     * @param value the elasticsearch property value to convert, must not be {@literal null}
     * @return The converted value, must not be {@literal null}
     */
    Object read(Object value);
}

下面直接上代码吧:

/**
 * 关于类的说明
 *
 * @Author lythen
 * @date 2023/6/6 11:34
 **/
@Slf4j
public class LocaDateTimeEsConverter implements PropertyValueConverter {
    /**
     * Converts a property value to an elasticsearch value. If the converter cannot convert the value, it must return a
     * String representation.
     *
     * @param value the value to convert, must not be {@literal null}
     * @return The elasticsearch property value, must not be {@literal null}
     */
    @Override
    public Object write(Object value) {
        if(value instanceof LocalDateTime ) {
                LocalDateTime localDateTime = (LocalDateTime)value;
                ZonedDateTime zonedDateTime = ZonedDateTime.of(localDateTime,ZoneId.of("GMT+8"));
                return zonedDateTime.toInstant().toString();
        }
        return  value;
    }

    /**
     * Converts an elasticsearch property value to a property value.
     *
     * @param value the elasticsearch property value to convert, must not be {@literal null}
     * @return The converted value, must not be {@literal null}
     */
    @Override
    public Object read(Object value) {
        try {
            String strDateTime = value.toString();
            Instant instant = null;
            if(value.getClass().getName().contains("Long")){
                long longTime = (Long) value;
                Date date = new Date(longTime);
                instant =date.toInstant();
            }
            else{
                instant = Instant.parse(strDateTime);
            }
            LocalDateTime localDateTime = LocalDateTime.ofInstant(instant,ZoneId.of("GMT+8"));
            return localDateTime;
        }catch (Exception e){
            log.error(e.getMessage(),e);
            return null;
        }
    }
}

转换的思路如下:

实体->ES,使用带区的时间类ZonedDateTime将原始时间识别为东八区时间(这里是识别,而非转换),以下为调试时的属性。这里的时间要和之前带Z的时间区分一下,这里能看到时间是“2023-06-06T14:56:50.880339300+08:00[GMT+08:00]”,这里表示这个是东八区的时间,而不是国际标准时间。最后zonedDateTime.toInstant(),这里其实是个取巧的方式,因为Instant自身也无时区概念,参考Instant - 廖雪峰的官方网站 (liaoxuefeng.com),它指的是时间戳,或者就理解为0时间区的时间也没什么问题,那使用zonedDateTime.toInstant()就是把东八区的时间转成了0时间区时间,再用个toString(),就转换成了UTC的国际标准时间,也就是少了8小时带Z的那个时间格式。

 ES->实体

这个就没什么好说了,其实比较好理解,主要就是遇到了存储为Long的时间,因此多做了一个判断,后面也就是从UTC国际标准时间,转换为LocalDateTime的过程,转换之后,加了8小时,那我们查询的结果就正确了。

最后,在实体加上注解:

    @Field(type = FieldType.Date, format = {DateFormat.date_hour_minute_second_millis})
    @JsonDeserialize(using = LocalDateTimeDeserializer.class)
    @JsonSerialize(using = LocalDateTimeSerializer.class)
    @ValueConverter(LocaDateTimeEsConverter.class)
    LocalDateTime ywblStartTime;

    @Field(type = FieldType.Date, format = {DateFormat.date_hour_minute_second_millis})
    @JsonDeserialize(using = LocalDateTimeDeserializer.class)
    @JsonSerialize(using = LocalDateTimeSerializer.class)
    @ValueConverter(LocaDateTimeEsConverter.class)
    LocalDateTime ywblEndTime;