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.
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:
É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()
));
}
}
É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;
}
}