使用 Spring Boot >= 2.0.1.RELEASE 将 ZonedDateTime 保存到 MongoDB 时出现 CodecConfigurationException
CodecConfigurationException when saving ZonedDateTime to MongoDB with Spring Boot >= 2.0.1.RELEASE
我能够通过对 Accessing Data with MongoDB, see https://github.com/thokrae/spring-data-mongo-zoneddatetime 的官方 Spring 引导指南进行最小修改来重现我的问题。
向客户 class、运行 添加 java.time.ZonedDateTime
字段后,指南中的示例代码失败并出现 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 引导版本从 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),但如果不需要更改此行为也不会具有高优先级,我会理解。
最好的解决方法是什么?当 duckduckgoing 寻找异常消息时,我发现了几种方法,例如注册 custom codec, a custom converter or using Jackson JSR 310。我宁愿不向我的项目添加自定义代码来处理 java.time 包中的 class。
Spring 数据 MongoDB 从未支持使用时区保留日期时间类型,正如 Oliver Drotbohm 本人在 DATAMONGO-2106.
中所述
这些是已知的解决方法:
- 使用不带时区的日期时间类型,例如java.time.Instant。 (通常建议只在后端使用 UTC,但我不得不扩展现有的代码库,它采用了不同的方法。)
编写自定义转换器并通过扩展 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()
));
}
}
编写自定义编解码器。至少在理论上。我的 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;
}
}
我能够通过对 Accessing Data with MongoDB, see https://github.com/thokrae/spring-data-mongo-zoneddatetime 的官方 Spring 引导指南进行最小修改来重现我的问题。
向客户 class、运行 添加 java.time.ZonedDateTime
字段后,指南中的示例代码失败并出现 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 引导版本从 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),但如果不需要更改此行为也不会具有高优先级,我会理解。
最好的解决方法是什么?当 duckduckgoing 寻找异常消息时,我发现了几种方法,例如注册 custom codec, a custom converter or using Jackson JSR 310。我宁愿不向我的项目添加自定义代码来处理 java.time 包中的 class。
Spring 数据 MongoDB 从未支持使用时区保留日期时间类型,正如 Oliver Drotbohm 本人在 DATAMONGO-2106.
中所述这些是已知的解决方法:
- 使用不带时区的日期时间类型,例如java.time.Instant。 (通常建议只在后端使用 UTC,但我不得不扩展现有的代码库,它采用了不同的方法。)
编写自定义转换器并通过扩展 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() )); } }
编写自定义编解码器。至少在理论上。我的 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; } }