🎬 第一阶段:发现问题(认识症状)
🔴 看到的错误日志:
2025-09-30 14:23:45.123 ERROR - ES全量同步任务执行失败
co.elastic.clients.transport.TransportException:
Missing [X-Elastic-Product] header.
Please check that you are connecting to an Elasticsearch instance,
error trace ElasticsearchException[Missing [X-Elastic-Product] header]
💭 我的第一反应:
思考1: "Missing header" - 缺少HTTP头部↓
思考2: 这个错误很具体,是ES客户端在检查某个头部↓
思考3: 先去Google搜一下这个错误
🔎 第二阶段:信息收集(10分钟)
搜索关键词:elasticsearch Missing X-Elastic-Product header
找到几个关键信息:
Stack Overflow:
"This error occurs when using Elasticsearch 8.x client with Elasticsearch 7.x server"GitHub Issue:
"ES 8.x client requires X-Elastic-Product header in responsebut ES 7.x server doesn't send it"官方文档:
"Starting from 8.0, the client validates the server by checking the X-Elastic-Product header"
💡 初步结论:
问题根源:
├─ ES 8.x 客户端有新的安全检查
├─ 要求服务器响应必须包含 X-Elastic-Product 头部
└─ ES 7.x 服务器不返回这个头部解决方向:
1. 升级服务器到8.x(成本高)
2. 降级客户端到7.x(依赖地狱)
3. 让客户端跳过检查(可能不安全)
4. 在响应中添加这个头部(?如何做到)
🧪 第三阶段:第一次尝试(方向错误)
❌ 尝试1:在请求中添加头部(15分钟)
我的想法:
"既然缺少头部,那我在请求中加上不就行了?"
修改代码:
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.add("X-Elastic-Product", "Elasticsearch");
结果:
还是报同样的错误!为什么?
仔细看错误信息:"Missing [X-Elastic-Product] header"
客户端是在检查响应,不是请求!❌ 方向错了!
💭 反思:
错误分析:
我理解错了问题
- 客户端发送请求 → 服务器
- 服务器返回响应 → 客户端
- 客户端检查响应头部 ← 这里出错了!所以:
✅ 需要修改的是:服务器的响应
❌ 不是:客户端的请求
🔄 第四阶段:调整思路(关键转折)
💡 新思路:
问题:客户端检查响应头↓
难点:我改不了服务器(ES 7.x本身)↓
想法:能不能在响应到达客户端之前,先"拦截"它,然后添加头部?↓
关键词:HTTP 拦截器(Interceptor)
🔍 搜索:apache httpclient response interceptor
找到了:
HttpResponseInterceptor
- 可以拦截HTTP响应
- 在响应到达客户端前处理
- 可以修改响应头部✅ 这就是我要找的!
🛠️ 第五阶段:第二次尝试(方向正确但实现有问题)
✅ 尝试2:使用响应拦截器(30分钟)
第一版代码:
HttpResponseInterceptor interceptor = new HttpResponseInterceptor() {@Overridepublic void process(HttpResponse response, HttpContext context) {response.addHeader("X-Elastic-Product", "Elasticsearch");}
};// 但是在哪里注册这个拦截器???
🤔 问题:拦截器应该注册在哪里?
查看ES客户端的层级结构:
调用顺序(从上到下):ElasticsearchClient(高层API)↓
RestClientTransport(传输层)↓
RestClient(HTTP客户端)← 在这里!↓
Apache HttpClient(底层HTTP)← 拦截器在这里工作
查找RestClient的API文档:
RestClient.builder(HttpHost...).setHttpClientConfigCallback(callback -> {// 在这里可以配置Apache HttpClient// 包括添加拦截器!})
💡 找到了注册位置!
📝 第六阶段:编写完整代码(20分钟)
完整实现:
@Bean
@Primary
public RestClient elasticsearchRestClient() {// 1. 创建响应拦截器HttpResponseInterceptor responseInterceptor = new HttpResponseInterceptor() {@Overridepublic void process(HttpResponse response, HttpContext context) {if (!response.containsHeader("X-Elastic-Product")) {response.addHeader("X-Elastic-Product", "Elasticsearch");log.debug("✅ 已添加 X-Elastic-Product 头部");}}};// 2. 创建RestClient,注册拦截器RestClientBuilder builder = RestClient.builder(new HttpHost(host, port, scheme)).setHttpClientConfigCallback(httpClientBuilder -> {// 3. 在这里添加拦截器到Apache HttpClienthttpClientBuilder.addInterceptorLast(responseInterceptor);return httpClientBuilder;});return builder.build();
}
🐛 第七阶段:编译和运行(遇到新问题)
❌ 问题3:Bean冲突(10分钟)
编译通过,但运行报错:
BeanCreationException:
Error creating bean with name 'elasticsearchClient':
Ambiguous factory method matches found
分析:
原因:
我的配置类继承了 ElasticsearchConfiguration↓
ElasticsearchConfiguration 已经提供了 elasticsearchClient bean↓
我又自己定义了一个 elasticsearchClient bean↓
Spring 不知道用哪个!
解决:
// 移除继承
// public class ElasticsearchClientConfig extends ElasticsearchConfiguration {// 改为
public class ElasticsearchClientConfig {// 手动定义所有需要的Bean
}
🔄 第八阶段:补全所有Bean(30分钟)
问题:移除继承后,缺少其他Bean
NoSuchBeanDefinitionException:
No qualifying bean of type
'org.springframework.data.elasticsearch.core.ElasticsearchOperations'
💭 分析Bean依赖关系:
需要的Bean链条:ElasticsearchOperations(Spring Data的核心接口)↓ 需要
ElasticsearchTemplate(实现类)↓ 需要
ElasticsearchClient(ES客户端)↓ 需要
RestClient(HTTP客户端)← 我已经有了还需要:
ElasticsearchConverter(数据转换器)
补全所有Bean:
@Bean
@Primary
public RestClient elasticsearchRestClient() {// 已有
}@Bean
@Primary
public ElasticsearchClient elasticsearchClient(RestClient restClient) {RestClientTransport transport = new RestClientTransport(restClient,new JacksonJsonpMapper());return new ElasticsearchClient(transport);
}@Bean
@Primary
public ElasticsearchConverter elasticsearchConverter() {return new MappingElasticsearchConverter(new SimpleElasticsearchMappingContext());
}@Bean
@Primary
public ElasticsearchOperations elasticsearchOperations(ElasticsearchClient elasticsearchClient,ElasticsearchConverter elasticsearchConverter) {return new ElasticsearchTemplate(elasticsearchClient,elasticsearchConverter);
}
🚀 第九阶段:首次成功运行(但还有其他问题)
✅ 响应拦截器工作了!
日志显示:
✅ 响应拦截器已添加头部: X-Elastic-Product=Elasticsearch
✅ Elasticsearch客户端验证通过
❌ 但出现了新错误:
java.lang.reflect.InaccessibleObjectException:
Unable to make field private final java.math.BigInteger
java.math.BigDecimal.intVal accessible:
module java.base does not "opens java.math" to unnamed module
🔧 第十阶段:解决BigDecimal问题(20分钟)
💭 分析新错误:
错误原因:
Java 17的模块系统限制了反射访问↓
Spring Data Elasticsearch在序列化时
需要反射访问BigDecimal的内部字段↓
但Java 17不允许这样做
尝试的解决方案:
方案A:添加JVM参数(不推荐)
--add-opens java.base/java.math=ALL-UNNAMED
✅ 可以工作,但是治标不治本
方案B:改用Double(推荐)
// 修改实体类
@Field(type = FieldType.Double)
private Double standardPrice; // 从 BigDecimal 改为 Double// 修改同步逻辑
esProduct.setStandardPrice(product.getStandardPrice() != null ? product.getStandardPrice().doubleValue() : null
);
✅ 彻底解决,不需要JVM参数
我选择了方案B
🧹 第十一阶段:清理旧代码(10分钟)
删除不需要的文件:
❌ ElasticsearchRequestOptionsConfig.java- 使用旧API(RequestOptions)- ES 8.x不再需要- 功能已被响应拦截器替代
更新配置文件:
# 禁用兼容性配置(因为已经在主配置中处理)
commerce:elasticsearch:compatibility:enabled: false
✅ 第十二阶段:全面测试和验证(30分钟)
测试清单:
✅ 1. 应用启动测试- ElasticsearchClientConfig 正确初始化- 所有Bean正确创建- 无错误日志✅ 2. ES连接测试- 响应拦截器正常工作- X-Elastic-Product头部正确添加- 客户端验证通过✅ 3. 数据同步测试- XXL-JOB任务触发- 商品数据正确同步- 价格字段正确转换(Double)- 返回码:200✅ 4. 搜索功能测试- 索引创建成功- 数据可以搜索- 性能正常