一尘不染

使用Spring Boot> = 2.0.1将ZonedDateTime保存到MongoDB时发生CodecConfigurationException

spring-boot

通过对用于使用MongoDB访问数据的官方Spring
Boot指南进行了最小的修改,我能够重现我的问题,请参阅https://github.com/thokrae/spring-data-mongo-
zoneddatetime。

在将java.time.ZonedDateTime字段添加到Customer类之后,从指南中运行示例代码失败,并出现CodecConfigurationException:

Customer.java:

    public String lastName;
    public ZonedDateTime created;

    public Customer() {

输出:

...
Caused by: org.bson.codecs.configuration.CodecConfigurationException`: Can't find a codec for class java.time.ZonedDateTime.
at org.bson.codecs.configuration.CodecCache.getOrThrow(CodecCache.java:46) ~[bson-3.6.4.jar:na]
at org.bson.codecs.configuration.ProvidersCodecRegistry.get(ProvidersCodecRegistry.java:63) ~[bson-3.6.4.jar:na]
at org.bson.codecs.configuration.ChildCodecRegistry.get(ChildCodecRegistry.java:51) ~[bson-3.6.4.jar:na]

这可以通过将pom.xml中的Spring Boot版本从2.0.5.RELEASE更改为2.0.1.RELEASE来解决:

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.1.RELEASE</version>
    </parent>

现在,异常消失了,并将包括ZonedDateTime字段的Customer对象写入MongoDB

我在spring-data-
mongodb项目中提交了一个错误(DATAMONGO-2106),但会了解是否不希望更改此行为,也不具有较高的优先级。

最好的解决方法是什么?当duckduckgoing为异常消息时,我找到了几种方法,如注册定制编解码器定制转换器或使用Jackson JSR 310。我宁愿不向项目添加自定义代码来处理java.time包中的类。


阅读 1195

收藏
2020-05-30

共1个答案

一尘不染

像Oliver Drotbohm自己在DATAMONGO-2106中所说的那样,Spring Data MongoDB从未支持使用时区保留日期时间类型。

这些是已知的解决方法:

  1. 使用不带时区的日期时间类型,例如java.time.Instant。(通常建议仅在后端使用UTC,但是我不得不扩展采用不同方法的现有代码库。)
  2. 编写一个自定义转换器,并通过扩展AbstractMongoConfiguration对其进行注册。有关运行示例,请参见我的测试库中的分支转换器。
@Component
@WritingConverter
public class ZonedDateTimeToDocumentConverter implements Converter<ZonedDateTime, Document> {
    static final String DATE_TIME = "dateTime";
    static final String ZONE = "zone";

    @Override
    public Document convert(@Nullable ZonedDateTime zonedDateTime) {
        if (zonedDateTime == null) return null;

        Document document = new Document();
        document.put(DATE_TIME, Date.from(zonedDateTime.toInstant()));
        document.put(ZONE, zonedDateTime.getZone().getId());
        document.put("offset", zonedDateTime.getOffset().toString());
        return document;
    }
}

@Component
@ReadingConverter
public class DocumentToZonedDateTimeConverter implements Converter<Document, ZonedDateTime> {

    @Override
    public ZonedDateTime convert(@Nullable Document document) {
        if (document == null) return null;

        Date dateTime = document.getDate(DATE_TIME);
        String zoneId = document.getString(ZONE);
        ZoneId zone = ZoneId.of(zoneId);

        return ZonedDateTime.ofInstant(dateTime.toInstant(), zone);
    }
}

@Configuration
public class MongoConfiguration extends AbstractMongoConfiguration {

    @Value("${spring.data.mongodb.database}")
    private String database;

    @Value("${spring.data.mongodb.host}")
    private String host;

    @Value("${spring.data.mongodb.port}")
    private int port;

    @Override
    public MongoClient mongoClient() {
        return new MongoClient(host, port);
    }

    @Override
    protected String getDatabaseName() {
        return database;
    }

    @Bean
    public CustomConversions customConversions() {
        return new MongoCustomConversions(asList(
                new ZonedDateTimeToDocumentConverter(),
                new DocumentToZonedDateTimeConverter()
        ));
    }
}
  1. 编写自定义编解码器。至少在理论上。使用Spring Boot 2.0.5时,我的编解码器测试分支无法解组数据,同时与Spring Boot 2.0.1正常工作。
public class ZonedDateTimeCodec implements Codec<ZonedDateTime> {

    public static final String DATE_TIME = "dateTime";
    public static final String ZONE = "zone";

    @Override
    public void encode(final BsonWriter writer, final ZonedDateTime value, final EncoderContext encoderContext) {
        writer.writeStartDocument();
        writer.writeDateTime(DATE_TIME, value.toInstant().getEpochSecond() * 1_000);
        writer.writeString(ZONE, value.getZone().getId());
        writer.writeEndDocument();
    }

    @Override
    public ZonedDateTime decode(final BsonReader reader, final DecoderContext decoderContext) {
        reader.readStartDocument();
        long epochSecond = reader.readDateTime(DATE_TIME);
        String zoneId = reader.readString(ZONE);
        reader.readEndDocument();

        return ZonedDateTime.ofInstant(Instant.ofEpochSecond(epochSecond / 1_000), ZoneId.of(zoneId));
    }

    @Override
    public Class<ZonedDateTime> getEncoderClass() {
        return ZonedDateTime.class;
    }
}

@Configuration
public class MongoConfiguration extends AbstractMongoConfiguration {

    @Value("${spring.data.mongodb.database}")
    private String database;

    @Value("${spring.data.mongodb.host}")
    private String host;

    @Value("${spring.data.mongodb.port}")
    private int port;

    @Override
    public MongoClient mongoClient() {
        return new MongoClient(host + ":" + port, createOptions());
    }

    private MongoClientOptions createOptions() {
        CodecProvider pojoCodecProvider = PojoCodecProvider.builder()
                .automatic(true)
                .build();

        CodecRegistry registry = CodecRegistries.fromRegistries(
                createCustomCodecRegistry(),
                MongoClient.getDefaultCodecRegistry(),
                CodecRegistries.fromProviders(pojoCodecProvider)
        );

        return MongoClientOptions.builder()
                .codecRegistry(registry)
                .build();
    }

    private CodecRegistry createCustomCodecRegistry() {
        return CodecRegistries.fromCodecs(
                new ZonedDateTimeCodec()
        );
    }

    @Override
    protected String getDatabaseName() {
        return database;
    }
}
2020-05-30