spring - 使用 Spring Boot >= 2.0.1.RELEASE 将 ZonedDateTime 保存到 MongoDB 时出现 CodecConfigurationException

标签 spring mongodb spring-boot spring-data-mongodb java-time

我能够通过对 Accessing Data with MongoDB 的官方 Spring Boot 指南进行最小修改来重现我的问题,见 https://github.com/thokrae/spring-data-mongo-zoneddatetime .

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

客户.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 字段 are written to MongoDB .

我向 spring-data-mongodb 项目提交了一个错误 (DATAMONGO-2106),但如果不希望更改此行为也没有高优先级,我会理解。

最好的解决方法是什么?在处理异常消息时,我发现了几种方法,例如注册 custom codec , 一个 custom converter或使用 Jackson JSR 310 .我宁愿不向我的项目添加自定义代码来处理 java.time 包中的类。

最佳答案

Spring Data MongoDB 从不支持带有时区的日期时间类型,正如 Oliver Drotbohm 本人在 DATAMONGO-2106 中所述.

这些是已知的解决方法:

  1. 使用不带时区的日期时间类型,例如java.time.Instant。 (通常建议只在后端使用 UTC,但我必须扩展现有的代码库,该代码库采用不同的方法。)
  2. 编写一个自定义转换器并通过扩展 AbstractMongoConfiguration 来注册它。见分行converter在我的测试存储库中运行示例。

    @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()
            ));
        }
    }
    
  3. 编写自定义编解码器。至少在理论上。我的 codec test branch使用 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;
        }
    }
    

关于spring - 使用 Spring Boot >= 2.0.1.RELEASE 将 ZonedDateTime 保存到 MongoDB 时出现 CodecConfigurationException,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/52677253/

相关文章:

java - Spring MVC - session 验证的正确方法

java - 为什么在将 Spring Boot 应用程序部署到 Openshift 时需要应用程序容器

java - Spring bean单例与单例模式

json - 在 Heroku 上使用 Node 读取、写入和存储 JSON

java - 工厂方法中的依赖注入(inject)导致 NullPointerException

mongodb - 如何使用 aggregate 按半小时分组并四舍五入?

javascript - Meteor 从模态访问数据

spring-mvc - 我可以使用 spring-boot-starter-tomcat 但不使用 spring-boot-starter-web 来提供静态内容吗?

java - 如何从 StateMachineConfigurer 创建 StateMachine

spring - hibernate 异常 : Use of DefaultSchemaNameResolver requires Dialect to provide the proper SQL statement/command