CodecConfigurationException при сохранении ZonedDateTime в MongoDB с Spring Boot >= 2.0.1.RELEASE
Я смог воспроизвести мою проблему с минимальной модификацией официального руководства Spring Boot по доступу к данным с MongoDB, см. 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]
Это можно решить, изменив версию Spring Boot с 2.0.5.RELEASE на 2.0.1.RELEASE в pom.xml:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.1.RELEASE</version>
</parent>
Теперь исключение прошло, и объекты Customer, включая поля ZonedDateTime , записываются в MongoDB.
Я подал ошибку ( DATAMONGO-2106) в проекте spring-data-mongodb, но пойму, что изменение этого поведения нежелательно и не имеет высокого приоритета.
Какой лучший обходной путь? При получении сообщения об исключении я обнаружил несколько подходов, таких как регистрация пользовательского кодека, пользовательского преобразователя или использование Jackson JSR 310. Я бы предпочел не добавлять пользовательский код в мой проект для обработки класса из пакета java.time.
1 ответ
Spring Data MongoDB никогда не поддерживал постоянные типы даты и времени с часовыми поясами, как заявил сам Оливер Дротбом в DATAMONGO-2106.
Это известные обходные пути:
- Используйте тип даты и времени без часового пояса, например, java.time.Instant. (Обычно рекомендуется использовать UTC только в бэкэнде, но мне пришлось расширить существующую кодовую базу, которая следовала другому подходу.)
Напишите пользовательский конвертер и зарегистрируйте его, расширив 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() )); } }
Написать собственный кодек. По крайней мере, в теории. Моя тестовая ветвь кодека не может разархивировать данные при использовании 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; } }