Elasticsearch noop更新响应无法反序列化的问题咨询
当执行脚本化更新(请求路径/my_idx/_update/my_doc_id)且条件未满足时,Elasticsearch 7.2.0返回如下noop响应:
{ "_index": "my_idx", "_type": "_doc", "_id": "my_doc_id", "_version": 4, "result": "noop", "_shards": { "total": 0, "successful": 0, "failed": 0 } }
但该响应无法被elasticsearch-java客户端(版本8.4.2)反序列化为UpdateResponse<TDocument>,原因是其父类WriteResponseBase的构造器强制要求primaryTerm和seqNo字段,而这两个字段在noop响应中缺失:
protected WriteResponseBase(AbstractBuilder<?> builder) { this.id = ApiTypeHelper.requireNonNull(builder.id, this, "id"); this.index = ApiTypeHelper.requireNonNull(builder.index, this, "index"); this.primaryTerm = ApiTypeHelper.requireNonNull(builder.primaryTerm, this, "primaryTerm"); this.result = ApiTypeHelper.requireNonNull(builder.result, this, "result"); this.seqNo = ApiTypeHelper.requireNonNull(builder.seqNo, this, "seqNo"); this.shards = ApiTypeHelper.requireNonNull(builder.shards, this, "shards"); this.type = builder.type; this.version = ApiTypeHelper.requireNonNull(builder.version, this, "version"); this.forcedRefresh = builder.forcedRefresh; }
已通过hack手段让Elasticsearch服务端7.2.0与Java客户端8.4.2基本兼容,现在需要确认两个问题:
primaryTerm和seqNo在Elasticsearch 7.2.0的noop响应中是否属于可选字段?- 除了捕获异常处理外,是否存在其他可行的解决办法?
1. 关于primaryTerm和seqNo的可选性
在Elasticsearch 7.2.0中,primaryTerm和seqNo确实是可选字段。这两个字段用于实现乐观并发控制和数据版本追踪,但仅在文档实际发生变更(如创建、更新、删除)时才会返回。当更新操作触发noop(无实际变更)时,服务端不会生成新的序列号和主分片任期,因此响应中不会包含这两个字段。
2. 除捕获异常外的解决办法
(1)自定义反序列化逻辑
通过Jackson的Module扩展客户端的反序列化规则,为WriteResponseBase类添加自定义反序列化器:
- 当响应中不存在
primaryTerm和seqNo时,默认赋值为0或其他合法默认值; - 注册该自定义模块到Elasticsearch客户端的
RestClientBuilder中,覆盖默认的反序列化行为。
示例代码片段:
public class WriteResponseBaseDeserializer extends StdDeserializer<WriteResponseBase> { public WriteResponseBaseDeserializer() { super(WriteResponseBase.class); } @Override public WriteResponseBase deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { JsonNode node = p.getCodec().readTree(p); // 提取必填字段 String id = node.get("_id").asText(); String index = node.get("_index").asText(); String result = node.get("result").asText(); ShardStatistics shards = p.getCodec().treeToValue(node.get("_shards"), ShardStatistics.class); long version = node.get("_version").asLong(); // 处理可选字段,默认值设为0 long primaryTerm = node.has("primary_term") ? node.get("primary_term").asLong() : 0; long seqNo = node.has("_seq_no") ? node.get("_seq_no").asLong() : 0; // 构建WriteResponseBase实例(借助反射绕过Builder的访问限制) try { Constructor<?> builderConstructor = Class.forName("org.elasticsearch.action.support.WriteResponseBase$Builder").getDeclaredConstructor(); builderConstructor.setAccessible(true); Object builder = builderConstructor.newInstance(); // 调用Builder的setter方法 Method setId = builder.getClass().getMethod("id", String.class); setId.invoke(builder, id); Method setIndex = builder.getClass().getMethod("index", String.class); setIndex.invoke(builder, index); Method setPrimaryTerm = builder.getClass().getMethod("primaryTerm", long.class); setPrimaryTerm.invoke(builder, primaryTerm); Method setResult = builder.getClass().getMethod("result", WriteResponse.Result.class); setResult.invoke(builder, WriteResponse.Result.valueOf(result.toUpperCase())); Method setSeqNo = builder.getClass().getMethod("seqNo", long.class); setSeqNo.invoke(builder, seqNo); Method setShards = builder.getClass().getMethod("shards", ShardStatistics.class); setShards.invoke(builder, shards); Method setVersion = builder.getClass().getMethod("version", long.class); setVersion.invoke(builder, version); Method buildMethod = builder.getClass().getMethod("build"); return (WriteResponseBase) buildMethod.invoke(builder); } catch (Exception e) { throw new IOException("Failed to deserialize WriteResponseBase", e); } } }
注册反序列化器到客户端:
RestClientBuilder builder = RestClient.builder(new HttpHost("localhost", 9200)); JacksonJsonpMapper jsonMapper = new JacksonJsonpMapper(); ObjectMapper objectMapper = jsonMapper.objectMapper(); SimpleModule module = new SimpleModule(); module.addDeserializer(WriteResponseBase.class, new WriteResponseBaseDeserializer()); objectMapper.registerModule(module); RestHighLevelClient client = new RestHighLevelClient( builder.setHttpClientConfigCallback(httpClientBuilder -> httpClientBuilder.setDefaultIOReactorConfig(IOReactorConfig.custom().setIoThreadCount(Runtime.getRuntime().availableProcessors()).build()) ).setMapper(jsonMapper) );
(2)修改客户端依赖的反序列化约束
通过字节码修改工具(如ASM、ByteBuddy)修改WriteResponseBase的构造器,移除primaryTerm和seqNo的非空校验:
- 定位到
WriteResponseBase类的构造器,将ApiTypeHelper.requireNonNull(builder.primaryTerm, this, "primaryTerm")替换为builder.primaryTerm != null ? builder.primaryTerm : 0; - 同理处理
seqNo字段,使其允许空值并赋予默认值。
这种方式需要注意客户端版本兼容性,升级客户端时需重新处理。
(3)在服务端添加默认字段
通过Elasticsearch的插件或自定义响应拦截器,在noop响应中主动添加primary_term和_seq_no字段,默认值设为0。但这种方式需要修改服务端逻辑,对于已上线的集群风险较高,需谨慎操作。
内容的提问来源于stack exchange,提问作者linusv

