web-dev-qa-db-fra.com

CodecConfigurationException lors de l'enregistrement de ZonedDateTime dans MongoDB avec Spring Boot> = 2.0.1.RELEASE

J'ai pu reproduire mon problème avec une modification minimale du guide officiel Spring Boot pour Accès aux données avec MongoDB , voir https://github.com/thokrae/spring-data-mongo -zoneddatetime .

Après avoir ajouté un Java.time.ZonedDateTime champ à la classe Customer, l'exécution de l'exemple de code du guide échoue avec une exception CodecConfigurationException:

Client.Java:

    public String lastName;
    public ZonedDateTime created;

    public Customer() {

production:

...
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]

Cela peut être résolu en changeant la version Spring Boot de 2.0.5.RELEASE à 2.0.1.RELEASE dans le pom.xml:

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

Maintenant, l'exception a disparu et les objets Customer, y compris les champs ZonedDateTime sont écrits dans MongoDB .

J'ai déposé un bogue ( DATAMONGO-2106 ) avec le projet spring-data-mongodb mais je comprendrais si changer ce comportement n'est pas souhaité et n'a pas une priorité élevée.

Quelle est la meilleure solution? Lorsque je passe pour le message d'exception, je trouve plusieurs approches comme l'enregistrement d'un codec personnalisé , un convertisseur personnalisé ou l'utilisation de Jackson JSR 31 . Je préférerais ne pas ajouter de code personnalisé à mon projet pour gérer une classe du package Java.time.

11
tdkBacke

La persistance des types de date et d'heure avec des fuseaux horaires n'a jamais été prise en charge par Spring Data MongoDB, comme l'a déclaré Oliver Drotbohm lui-même dans DATAMONGO-2106 .

Voici les solutions de contournement connues:

  1. Utilisez un type date/heure sans fuseau horaire, par ex. Java.time.Instant. (Il est généralement conseillé de n'utiliser que l'UTC dans le backend, mais j'ai dû étendre une base de code existante qui suivait une approche différente.)
  2. Écrivez un convertisseur personnalisé et enregistrez-le en étendant AbstractMongoConfiguration. Voir la branche convertisseur dans mon référentiel de test pour un exemple en cours d'exécution.

    @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. Écrivez un codec personnalisé. Du moins en théorie. Mon branche de test du codec ne peut pas démasquer les données lors de l'utilisation de Spring Boot 2.0.5 tout en fonctionnant correctement avec 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;
        }
    }
    
7
tdkBacke