Spring Boot 2.x 和 3.x 集成方式有什么区别?
ElasticsearchRepository 到底要不要继承?
ElasticsearchOperations 和 ElasticsearchRestTemplate 有何不同?
Spring Boot 2.x集成ES
maven依赖
<!-- Spring Data Elasticsearch -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
配置文件(application.yml)
spring:
elasticsearch:
rest:
uris: http://es-node1:9200,http://es-node2:9200,http://es-node3:9200
username: elastic
password: your-secure-password
connection-timeout: 1s
socket-timeout: 30s
connection-request-timeout: 1s
max-in-flight-requests-per-route: 50
max-in-flight-requests: 200
Java 配置类(核心:支持 HTTPS + 认证)
创建配置类,手动注册 RestClient 和 ElasticsearchRestTemplate Bean。
// ElasticsearchConfig.java
package com.example.demo.config;
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.json.jackson.JacksonJsonpMapper;
import co.elastic.clients.transport.rest_client.RestClientTransport;
import org.apache.http.HttpHost;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.nio.client.HttpAsyncClientBuilder;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestClientBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;
import org.springframework.util.StringUtils;
import java.time.Duration;
import java.util.Arrays;
@Configuration
public class ElasticsearchConfig {
@Value("${spring.elasticsearch.rest.uris}")
private String uris;
@Value("${spring.elasticsearch.rest.username:}")
private String username;
@Value("${spring.elasticsearch.rest.password:}")
private String password;
@Value("${spring.elasticsearch.rest.connection-timeout:1s}")
private Duration connectTimeout; // e.g., 1s
@Value("${spring.elasticsearch.rest.socket-timeout:30s}")
private Duration socketTimeout; // e.g., 30s
@Value("${spring.elasticsearch.rest.connection-request-timeout:1s}")
private Duration connectionRequestTimeout; // e.g., 1s
@Value("${spring.elasticsearch.rest.max-in-flight-requests-per-route:50}")
private int maxPerRoute;
@Value("${spring.elasticsearch.rest.max-in-flight-requests:200}")
private int maxTotal;
/**
* 构建生产级 RestClient
*/
@Bean
public RestClient restClient() {
// 1. 解析多个 ES 节点地址
HttpHost[] hosts = Arrays.stream(uris.split(","))
.map(String::trim)
.filter(s -> !s.isEmpty())
.map(HttpHost::create)
.toArray(HttpHost[]::new);
if (hosts.length == 0) {
throw new IllegalArgumentException("Elasticsearch hosts cannot be empty");
}
// 2. 认证配置(如果启用了安全)
final CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
if (username != null && !username.trim().isEmpty()) {
credentialsProvider.setCredentials(AuthScope.ANY,
new UsernamePasswordCredentials(username, password));
}
// 3. 创建连接管理器(真正的连接池)
PoolingAsyncClientConnectionManager connectionManager;
try {
IOReactorConfig ioReactorConfig = IOReactorConfig.custom()
.setConnectTimeout(Math.toIntExact(connectTimeout.toMillis()))
.build();
connectionManager = new PoolingAsyncClientConnectionManager(
new DefaultConnectingIOReactor(ioReactorConfig));
} catch (IOReactorException e) {
throw new RuntimeException("Failed to create IOReactor", e);
}
// 设置连接池大小
connectionManager.setMaxTotal(maxTotal);
connectionManager.setDefaultMaxPerRoute(maxPerRoute);
// 4. 构建 RestClient
RestClientBuilder builder = RestClient.builder(hosts)
.setHttpClientConfigCallback(httpClientBuilder -> {
HttpAsyncClientBuilder asyncClientBuilder = httpClientBuilder
.setConnectionManager(connectionManager)
.setDefaultCredentialsProvider(credentialsProvider)
// 设置连接存活时间(Keep-Alive)
.setConnectionTimeToLive(connectTimeout)
// 可选:自定义 Keep-Alive 策略
.setKeepAliveStrategy((response, context) ->
Duration.ofMinutes(5).toMillis());
// 🔐 如果是 HTTPS 且自签名证书(仅测试环境!生产请用可信CA)
/*
if (uris.startsWith("https")) {
try {
SSLContext sslContext = SSLContexts.custom()
.loadTrustMaterial(null, (chain, authType) -> true)
.build();
asyncClientBuilder.setSSLContext(sslContext);
asyncClientBuilder.setSSLStrategy(new SSLIOSessionStrategy(sslContext,
(hostname, sslSession) -> true)); // 忽略主机名验证(仅测试)
} catch (Exception e) {
throw new RuntimeException("Failed to create SSL context", e);
}
}
*/
return asyncClientBuilder;
})
.setRequestConfigCallback(requestConfigBuilder ->
requestConfigBuilder
// 建立连接超时
.setConnectTimeout(Math.toIntExact(connectTimeout.toMillis()))
// 从连接池获取连接的超时(关键!防无限等待)
.setConnectionRequestTimeout(Math.toIntExact(connectionRequestTimeout.toMillis()))
// 读取响应超时
.setSocketTimeout(Math.toIntExact(socketTimeout.toMillis()))
);
return builder.build();
}
@Bean
public ElasticsearchOperations elasticsearchOperations(RestClient restClient) {
RestClientTransport transport = new RestClientTransport(restClient, new JacksonJsonpMapper());
return new ElasticsearchRestTemplate(transport);
}
// 同时也可以提供 Reactive 版本(可选)
@Bean
public ReactiveElasticsearchOperations reactiveElasticsearchOperations(RestClient restClient) {
RestClientTransport transport = new RestClientTransport(restClient, new JacksonJsonpMapper());
return new ReactiveElasticsearchTemplate(transport);
}
}
实体类
// Product.java
package com.example.demo.entity;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
@Document(indexName = "products")
public class Product {
@Id
private String id;
@Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart")
private String name;
@Field(type = FieldType.Keyword)
private String category;
@Field(type = FieldType.Double)
private Double price;
@Field(type = FieldType.Integer)
private Integer stock;
// 构造函数
public Product() {}
public Product(String name, String category, Double price, Integer stock) {
this.name = name;
this.category = category;
this.price = price;
this.stock = stock;
}
// Getters and Setters
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getCategory() { return category; }
public void setCategory(String category) { this.category = category; }
public Double getPrice() { return price; }
public void setPrice(Double price) { this.price = price; }
public Integer getStock() { return stock; }
public void setStock(Integer stock) { this.stock = stock; }
@Override
public String toString() {
return "Product{" +
"id='" + id + '\'' +
", name='" + name + '\'' +
", category='" + category + '\'' +
", price=" + price +
", stock=" + stock +
'}';
}
}
Repository 接口
// ProductRepository.java
package com.example.demo.repository;
import com.example.demo.entity.Product;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface ProductRepository extends ElasticsearchRepository<Product, String> {
List<Product> findByNameContaining(String name);
List<Product> findByCategoryAndPriceBetween(String category, Double minPrice, Double maxPrice);
}
服务层
// ProductService.java
package com.example.demo.service;
import com.example.demo.entity.Product;
import com.example.demo.repository.ProductRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class ProductService {
@Autowired
private ProductRepository productRepository;
@Autowired
//@Qualifier("reactiveElasticsearchOperations")
private ElasticsearchOperations elasticsearchOperations;
//@Autowired
//private ElasticsearchRestTemplate ElasticsearchRestTemplate;
public Product save(Product product) {
return productRepository.save(product);
}
public List<Product> findAll() {
return productRepository.findAll();
}
public List<Product> searchByName(String name) {
return productRepository.findByNameContaining(name);
}
public List<Product> findByCategoryAndPriceRange(String category, Double min, Double max) {
return productRepository.findByCategoryAndPriceBetween(category, min, max);
}
public void deleteById(String id) {
productRepository.deleteById(id);
}
/**
* 使用 ElasticsearchOperations 执行复杂查询(如聚合)
*/
public void printCategoryAggregation() {
// 构建原生查询(可自定义 DSL)
NativeSearchQuery query = new NativeSearchQuery(Query.findAll());
query.addAggregation(Aggregation.builder("by_category")
.terms("category.keyword") // 按 keyword 字段聚合
.build());
// 执行聚合查询
SearchHits<Product> hits = elasticsearchOperations.search(query, Product.class);
// 输出聚合结果
hits.getAggregations().asMap().forEach((name, aggregation) -> {
System.out.println("Aggregation: " + name);
// 此处可解析 Terms、Histogram 等
});
}
}
启动类
// Application.java
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories;
@SpringBootApplication
@EnableElasticsearchRepositories(basePackages = "com.example.demo.repository")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
ElasticsearchOperations 和 ElasticsearchRestTemplate 有何不同?
ElasticsearchOperations
是接口,而ElasticsearchRestTemplate
是具体实现类。使用ElasticsearchOperations
面向编程的思想,你可以有多个实现类,实际用时选择具体用哪一个。
ElasticsearchRepository 到底要不要继承?
上面配置中使用ProductRepository
继承了ElasticsearchRepository
。
为什么不直接在service层使用ElasticsearchRestTemplate
操作ES
。原因如下:
- 继承
ElasticsearchRepository
会提供很多默认方法,就像mybatis-plus
,提供基本了crud
方法 - 使用
ElasticsearchRepository
时底层还是使用ElasticsearchRestTemplate
处理的,没必要重复造轮子 - 处理复杂查询时,service层可以同时使用
ElasticsearchRestTemplate
- 最佳实践就是同时使用,简单操作时使用
ElasticsearchRepository
,复杂查询时使用ElasticsearchRestTemplate
Spring Boot 3.x集成ES
去掉Java 配置类(核心:支持 HTTPS + 认证)
这一步即可。
引入maven依赖,application.yml文件中配置连接信息、认证信息便会自动完成配置。
Spring Boot 2.x 和 3.x 集成方式有什么区别?
2.x版本不支持通过配置文件中的用户名和密码自动完成认证,必须通过配置类完成手动认证
3.x版本支持 application.yml 直接配置,在配置文件中配置密码会自动完成认证,无需再手写配置类。
配置信息解读
1. connection-timeout: 1s
连接建立超时时间
✅ 含义:客户端尝试与 Elasticsearch 节点建立 TCP 连接的最大等待时间。
📌 类比:打电话拨号后,对方多久没接就挂断。
💡 示例:
如果 ES 节点宕机或网络不通,1 秒内无法完成三次握手,则抛出 ConnectTimeoutException。
✅ 默认值:1s
✅ 推荐值:1s ~ 5s
❌ 太大(如 30s):线程长时间阻塞,容易导致雪崩
❌ 太小(如 100ms):在网络抖动时频繁失败
⚠️ 这个“连接”指的是从你的应用服务器到 ES 节点的 TCP 连接。
2. socket-timeout: 30s
套接字超时 / 读取超时(Socket Timeout)
✅ 含义:连接建立成功后,等待数据返回的最大时间。即:发送请求后,最多等多久没收到响应就超时。
📌 类比:电话接通了,但对方一直不说话,等多久你就挂电话。
💡 示例:
你查询一个大数据量的聚合,ES 处理了 35 秒还没返回结果 → 客户端抛出 SocketTimeoutException
✅ 默认值:30s
✅ 推荐值:10s ~ 60s(根据业务复杂度调整)
❌ 太大:请求堆积,线程池耗尽
❌ 太小:复杂查询被误判为失败
⚠️ 这是最常见的超时类型,尤其在聚合、排序、深分页场景下容易触发。
3. connection-request-timeout: 1s
获取连接请求超时
✅ 含义:当连接池满时,新请求等待空闲连接的最大时间。
📌 类比:银行大厅满了,你在门口排队等座位,最多等多久就放弃。
💡 示例:
连接池最大 100 个连接,当前全部被占用 → 新请求开始排队
如果 1 秒内没人释放连接 → 抛出 ConnectionPoolTimeoutException
✅ 推荐值:500ms ~ 2s
✅ 默认值:-1 (无限等待,⚠️ 危险!)
❌ 太大:线程长时间等待,拖垮整个服务
✅ 设置它能防止“雪崩效应”——一个慢请求拖死整个系统
⚠️ 这是你配置中最容易遗漏但最关键的一项!
4. max-in-flight-requests-per-route: 10
每个路由(节点)最大并发请求数
✅ 含义:对同一个 ES 节点(如 http://es-node1:9200),最多同时发送多少个请求。
💡 作用:
防止单个节点被压垮
控制客户端对每个节点的负载
📌 “飞行中请求” = 已发出但未收到响应的请求
✅ 默认值:10
✅ 推荐值:10 ~ 50(取决于节点性能)
❌ 太大:可能打满单个节点的线程池
❌ 太小:无法充分利用并发能力
🎯 比如你有 3 个 ES 节点,该值为 10,则每个节点最多处理 10 个并发请求。
5. max-in-flight-requests: 100
整个客户端最大并发请求数
✅ 含义:客户端总共允许多少个“飞行中”的请求(所有节点加起来)。
💡 作用:
全局控制客户端的并发压力
防止客户端自己资源耗尽(如线程、内存)
✅ 默认值:100
✅ 推荐值:50 * 节点数 或根据 QPS 和 RT 估算
❌ 太大:客户端可能 OOM 或 CPU 过高
❌ 太小:吞吐量上不去
📊 计算公式参考:
最大并发 ≈ QPS × 平均响应时间(秒)
例如:QPS=200,RT=200ms → 需要约 40 个并发连接。
🧩 四者关系图解
深色版本
[你的应用]
│
├─ connection-request-timeout: 等待连接池放回连接 → 超时则失败
│
▼
[连接池] ── connection-timeout: 建立 TCP 连接 → 超时则失败
│
▼
[ES 节点] ── socket-timeout: 等待响应数据 → 超时则失败
│
◄─── max-in-flight-requests-per-route: 每个节点最多并发数
│
◄─── max-in-flight-requests: 所有节点总并发上限
6、配置要求
Spring Boot ≥ 2.3在yml文件中配置即可完整自动配置。
Spring Boot ≤ 2.2必须在配置类中手动配置。