Spring Boot 2.7.6整合ElasticSearch时java.time.Instant反序列化失败
问题描述
Spring Boot 2.7.6(基于JDK17)项目中,使用Elasticsearch Java Client 8.5.1查询数据时,实体类中的Instant字段无法正常反序列化,抛出如下错误:
2022-12-07 00:43:14.442 ERROR 59384 --- [nio-8080-exec-2] o.a.c.c.C.[.[.[.[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [/api/v1] threw exception [Request processing failed; nested exception is co.elastic.clients.json.JsonpMappingException: Error deserializing co.elastic.clients.elasticsearch.core.GetResponse: jakarta.json.JsonException: Jackson exception (JSON path: _source) (line no=1, column no=280, offset=-1)] with root cause com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Java 8 date/time type `java.time.Instant` not supported by default: add Module "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" to enable handling at [Source: (org.apache.http.nio.entity.ContentInputStream); line: 1, column: 279] (through reference chain: test.com.model.Project["createdDate"]) at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:67) ~[jackson-databind-2.14.1.jar:2.14.1] at com.fasterxml.jackson.databind.DeserializationContext.reportBadDefinition(DeserializationContext.java:1909) ~[jackson-databind-2.14.1.jar:2.14.1] at com.fasterxml.jackson.databind.deser.impl.UnsupportedTypeDeserializer.deserialize(UnsupportedTypeDeserializer.java:48) ~[jackson-databind-2.14.1.jar:2.14.1] at com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeAndSet(MethodProperty.java:129) ~[jackson-databind-2.14.1.jar:2.14.1] at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:314) ~[jackson-databind-2.14.1.jar:2.14.1] at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:177) ~[jackson-databind-2.14.1.jar:2.14.1] at com.fasterxml.jackson.databind.deser.DefaultDeserializationContext.readRootValue(DefaultDeserializationContext.java:323) ~[jackson-databind-2.14.1.jar:2.14.1] at com.fasterxml.jackson.databind.ObjectMapper._readValue(ObjectMapper.java:4706) ~[jackson-databind-2.14.1.jar:2.14.1] at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:2879) ~[jackson-databind-2.14.1.jar:2.14.1] at co.elastic.clients.json.jackson.JacksonJsonpMapper$JacksonValueParser.deserialize(JacksonJsonpMapper.java:123) ~[elasticsearch-java-8.5.1.jar:na] at co.elastic.clients.json.JsonpDeserializer.deserialize(JsonpDeserializer.java:76) ~[elasticsearch-java-8.5.1.jar:na] at co.elastic.clients.json.JsonpMapperBase.deserialize(JsonpMapperBase.java:70) ~[elasticsearch-java-8.5.1.jar:na] at co.elastic.clients.json.JsonpDeserializer$1.deserialize(JsonpDeserializer.java:99) ~[elasticsearch-java-8.5.1.jar:na] at co.elastic.clients.json.NamedDeserializer.deserialize(NamedDeserializer.java:64) ~[elasticsearch-java-8.5.1.jar:na] at co.elastic.clients.json.ObjectDeserializer$FieldObjectDeserializer.deserialize(ObjectDeserializer.java:78) ~[elasticsearch-java-8.5.1.jar:na] at co.elastic.clients.json.ObjectDeserializer.deserialize(ObjectDeserializer.java:192) ~[elasticsearch-java-8.5.1.jar:na] at co.elastic.clients.json.ObjectDeserializer.deserialize(ObjectDeserializer.java:148) ~[elasticsearch-java-8.5.1.jar:na] at co.elastic.clients.json.JsonpDeserializer.deserialize(JsonpDeserializer.java:76) ~[elasticsearch-java-8.5.1.jar:na] at co.elastic.clients.json.ObjectBuilderDeserializer.deserialize(ObjectBuilderDeserializer.java:79) ~[elasticsearch-java-8.5.1.jar:na] at co.elastic.clients.json.DelegatingDeserializer$SameType.deserialize(DelegatingDeserializer.java:43) ~[elasticsearch-java-8.5.1.jar:na] at co.elastic.clients.json.DelegatingDeserializer$SameType.deserialize(DelegatingDeserializer.java:43) ~[elasticsearch-java-8.5.1.jar:na] at co.elastic.clients.transport.endpoints.EndpointWithResponseMapperAttr$1.deserialize(EndpointWithResponseMapperAttr.java:56) ~[elasticsearch-java-8.5.1.jar:na] at co.elastic.clients.transport.rest_client.RestClientTransport.decodeResponse(RestClientTransport.java:325) ~[elasticsearch-java-8.5.1.jar:na] at co.elastic.clients.transport.rest_client.RestClientTransport.getHighLevelResponse(RestClientTransport.java:295) ~[elasticsearch-java-8.5.1.jar:na] at co.elastic.clients.transport.rest_client.RestClientTransport.performRequest(RestClientTransport.java:148) ~[elasticsearch-java-8.5.1.jar:na] at co.elastic.clients.elasticsearch.ElasticsearchClient.get(ElasticsearchClient.java:831) ~[elasticsearch-java-8.5.1.jar:na] at co.elastic.clients.elasticsearch.ElasticsearchClient.get(ElasticsearchClient.java:847) ~[elasticsearch-java-8.5.1.jar:na] ... .....
已尝试无效的方案
最初以为Spring Boot默认的Jackson配置能处理Java 8时间类型,后来参考资料编写了Jackson配置类,但问题依旧:
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import org.openapitools.jackson.nullable.JsonNullableModule; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.annotation.PostConstruct; @Configuration public class JacksonConfiguration { private final ObjectMapper objectMapper; public JacksonConfiguration(ObjectMapper objectMapper) { this.objectMapper = objectMapper; } @Bean public JavaTimeModule dateTimeModule(){ return new JavaTimeModule(); } @PostConstruct ObjectMapper jacksonObjectMapper() { objectMapper.registerModule(new JavaTimeModule()); objectMapper.registerModule(new JsonNullableModule()); return objectMapper; } }
问题原因
Elasticsearch Java Client(8.x版本)使用的是独立的JacksonJsonpMapper,而非Spring容器中管理的ObjectMapper。所以之前对Spring容器内ObjectMapper的配置,不会作用于Elasticsearch客户端的序列化逻辑。
解决方案
1. 确认依赖正确性
确保jackson-datatype-jsr310依赖已添加,且版本与jackson-databind一致(你的项目中已满足,版本均为2.14.1):
implementation group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: '2.14.1'
2. 自定义ElasticsearchClient配置,注入带JavaTimeModule的ObjectMapper
创建Elasticsearch客户端配置类,手动初始化带有JavaTimeModule的JacksonJsonpMapper,并以此构建ElasticsearchClient:
import co.elastic.clients.elasticsearch.ElasticsearchClient; import co.elastic.clients.json.jackson.JacksonJsonpMapper; import co.elastic.clients.transport.rest_client.RestClientTransport; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import org.apache.http.HttpHost; import org.elasticsearch.client.RestClient; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class ElasticsearchConfig { @Bean public ElasticsearchClient elasticsearchClient() { // 初始化RestClient,替换为你的ES地址 RestClient restClient = RestClient.builder( new HttpHost("localhost", 9200, "http") ).build(); // 配置ObjectMapper,注册JavaTimeModule处理Instant等Java 8时间类型 ObjectMapper objectMapper = new ObjectMapper(); objectMapper.registerModule(new JavaTimeModule()); // 如果需要处理空值字段,可添加JsonNullableModule // objectMapper.registerModule(new JsonNullableModule()); // 构建自定义的JacksonJsonpMapper JacksonJsonpMapper jsonpMapper = new JacksonJsonpMapper(objectMapper); // 初始化Transport和ElasticsearchClient RestClientTransport transport = new RestClientTransport(restClient, jsonpMapper); return new ElasticsearchClient(transport); } }
3. 清理依赖冲突
你的项目中同时引入了elasticsearch-rest-high-level-client:7.17.6和elasticsearch-java:8.5.1,跨大版本依赖会引发兼容性问题。建议移除elasticsearch-rest-high-level-client依赖,Elasticsearch Java Client 8.x已完全替代它的功能。
验证
重启项目后,再次执行Elasticsearch查询,Instant字段应能正常反序列化,不再抛出错误。
内容的提问来源于stack exchange,提问作者MetaCoder

