Spring Boot集成Elasticsearch完整配置与实战Demo
简介:本文详细介绍如何在Spring Boot应用中集成并配置Elasticsearch,实现高效的搜索与实时数据分析功能。通过添加Spring Data Elasticsearch依赖、配置集群连接、定义实体类与Repository接口,开发者可快速实现CRUD操作与自定义查询。内容涵盖依赖引入、配置文件设置、实体映射、数据访问层设计、服务调用、测试策略、性能优化、异常处理及安全控制等关键环节。本Demo经过验证,适用于构建可扩展的Java搜索系统,为后续高可用与大规模数据检索应用提供基础支撑。
Spring Boot 与 Elasticsearch 集成:从零搭建到生产级实战
在当今这个数据爆炸的时代,无论是电商平台的商品搜索、日志系统的实时分析,还是内容推荐引擎的个性化推送,背后都离不开一个核心能力—— 高效的数据检索与处理 。而提到“搜索”,就绕不开那个名字响当当的技术明星: Elasticsearch 。
但光有强大的搜索引擎还不够。现代开发讲求的是“快”和“稳”。谁都不想为了搭个搜索功能,写一堆配置、调半天连接、再手动处理序列化问题……这时候,Spring Boot 出场了。
🎯 想象一下这样的场景:
你刚接手一个新项目,老板说:“下周上线前要把所有商品支持模糊搜索,还得能按价格区间、分类筛选。”
你心里一紧:数据库查得慢,模糊匹配还影响性能……怎么办?
别慌!把 Spring Boot + Elasticsearch 组合搬出来,分分钟搞定!
这不是神话,而是无数团队正在用的事实。这两个技术的结合,就像给你的应用装上了“火箭推进器”——既保留了 Spring 生态的简洁优雅,又获得了 ES 的超强搜索能力。
今天,我们就来一场深度实战之旅,带你从零开始,手把手构建一个可落地、能扛住高并发的搜索系统。不只是“能跑”,更要“跑得稳、看得清、管得住”。
🧰 环境搭建:不是加个依赖那么简单
很多人以为集成 Elasticsearch 就是 pom.xml 里加一行依赖,然后写个 Repository 接口完事。错!这只能让你的程序“跑起来”,但离“生产可用”还差得远。
真正的环境搭建,是一场关于版本、安全、性能和资源管理的综合战役。
1. 选对武器:依赖怎么加才不踩坑?
先来看最基础的一环:引入依赖。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
看着简单吧?但你知道这一行代码背后藏着多少“暗流”吗?
它会自动帮你带上这些关键组件:
- REST 客户端(新版是 Java API Client)
- Spring Data 抽象层
- Jackson JSON 处理器
- 自动配置类 & Repository 支持
👉 这就是 Spring Boot 的魅力:“开箱即用”。但前提是—— 你得站在正确的版本船上 。
⚠️ 版本匹配有多重要?看这张表你就明白了👇
| Spring Boot 版本 | Spring Data Elasticsearch | ES Server | 默认客户端类型 |
|---|---|---|---|
| 2.7.x | 4.4.x | 7.17.x | RestHighLevelClient |
| 3.0.x ~ 3.2.x | 5.1.x | 8.7.x | Java API Client ✅ |
| 3.3+ | 5.2+ | 8.9+ | Java API Client |
💡 温馨提示:如果你用了 Spring Boot 3.x,请务必接受现实—— RestHighLevelClient 已被正式抛弃 。别再试图强行回滚版本,那只会埋下更多雷。
举个例子,假设你在 Spring Boot 3.1.5 中用了上面那个 starter,默认就会拉下来:
- spring-data-elasticsearch:5.1.5
- co.elastic.clients:elasticsearch-java:8.7.0
- jakarta.json-api:2.1.1
这套组合拳保证了 TLS 加密通信、JSON 序列化一致性以及新的类型安全 DSL 构建方式。
🚫 如果你硬要塞进去一个老掉牙的 Transport Client(比如基于 7.x 的),等着你的将是:
NoNodeAvailableException: None of the configured nodes are available
或者更诡异的:
InvalidResponseException: Expected a boolean, but found 'true '
——因为协议都不一样了,人家说 HTTP/REST,你还想走 TCP 传输,能通才怪!
所以啊, 版本治理不是小事 。建议做法是在 dependencyManagement 中统一锁定版本,避免冲突。
2. 可选依赖加持:让测试不再“本地能跑,线上炸锅”
除了主依赖,还有几个“神兵利器”值得加入你的工具箱:
| 依赖 | 用途 | 是否推荐 |
|---|---|---|
jackson-databind |
增强复杂对象序列化 | ✅ 推荐 |
testcontainers + elasticsearch |
启动真实 ES 实例用于集成测试 | 🚀 必备 |
spring-boot-starter-test |
支持@SpringBootTest | 开发标配 |
logback-encoder |
输出结构化日志到 ES | 生产优选 |
特别是 Testcontainers,简直是 CI/CD 的救星。
@Container
public static ElasticsearchContainer esContainer =
new ElasticsearchContainer("docker.elastic.co/elasticsearch/elasticsearch:8.7.0")
.withEnv("discovery.type", "single-node")
.withExposedPorts(9200);
有了它,你的测试不再是“模拟请求”,而是直接连上一个真实的单节点 ES 集群。再也不用担心“在我机器上好好的”这种尴尬局面。
3. 配置文件:别再把密码写进 YAML!
接下来是 application.yml 的配置环节。
最简单的写法长这样:
spring:
elasticsearch:
uris: http://localhost:9200
username: elastic
password: changeme
看起来没问题?NONONO!有几个致命细节你可能忽略了:
- 🔐 明文密码不能出现在配置文件中!
- 🔗 协议必须显式声明
http://或https:// - 🛡️ 8.x 版本默认启用 SSL/TLS,只用 HTTP 会直接拒绝连接!
✅ 正确姿势应该是:
spring:
elasticsearch:
uris: https://es-cluster.internal:9200
username: svc-search-user
password: ${ES_PWD} # 来自外部注入
然后通过环境变量或 K8s Secret 注入真实密码:
export ES_PWD="my-super-secret-password"
这才是生产级的安全意识。
4. 超时与连接池:防止线程卡死的隐形杀手
网络不稳定?查询太慢?用户一直在转圈?
很多时候,并不是 ES 不给力,而是你的客户端没做好“自我保护”。
Spring Boot 并不会暴露所有底层参数,所以我们需要手动定制:
@Bean
public RestClientBuilderCustomizer restClientBuilderCustomizer() {
return builder -> {
builder.setRequestConfigCallback(requestConfig -> requestConfig
.setConnectTimeout(5000) // 5秒连不上就放弃
.setSocketTimeout(10000) // 数据读取最多等10秒
.setContentCompressionEnabled(true)
);
builder.setHttpClientConfigCallback(httpClient -> httpClient
.setMaxConnTotal(100) // 总连接数上限
.setMaxConnPerRoute(20) // 每个 host 最多20个连接
);
};
}
📌 关键参数建议值:
| 参数 | 推荐值 | 说明 |
|---|---|---|
connectTimeout |
3~5s | 避免建连阶段无限等待 |
socketTimeout |
10~30s | 防止大查询拖垮整个线程池 |
maxConnTotal |
50~100 | 控制整体资源占用 |
maxConnPerRoute |
20~30 | 防止单个节点被打爆 |
这些设置在电商商品搜索、日志平台这类高并发场景中尤为重要。
5. 安全加固:SSL 认证怎么做才靠谱?
生产环境不用 HTTPS?等于裸奔。
Elasticsearch 6.8+ 开始内置 X-Pack Security,要求强制开启 HTTPS 和身份验证。
Java 层也要同步配置证书信任链:
@Bean
public RestClientBuilderCustomizer sslCustomizer(@Value("${cert.path}") String certPath) {
return builder -> {
try {
Path caCert = Paths.get(certPath);
SSLContext sslContext = SSLContextBuilder.create()
.loadTrustMaterial(caCert, null, (chains, authType) -> true) // 测试可用,生产慎用
.build();
builder.setHttpClientConfigCallback(httpClientBuilder ->
httpClientBuilder.setSSLContext(sslContext)
);
} catch (Exception e) {
throw new RuntimeException("Failed to initialize SSL context", e);
}
};
}
🔐 安全建议三连击:
1. 绝对不要在代码里 hardcode 密码;
2. 使用 Vault / AWS Secrets Manager / Kubernetes Secrets 管理密钥;
3. 对外服务走反向代理(如 Nginx)做统一鉴权,减少暴露面。
6. 客户端初始化机制揭秘:Spring 是如何“偷偷”帮你干活的?
你以为 @EnableAutoConfiguration 只是个注解?其实它是幕后指挥官。
以 Spring Boot 2.x 为例,核心自动配置类是 ElasticsearchRestClientAutoConfiguration ,它的任务包括:
- 解析
spring.elasticsearch.uris - 构建
RestClient - 包装成
RestHighLevelClient(已弃用) - 注册
ElasticsearchTemplate和ElasticsearchConverter
整个流程可以用一张 Mermaid 图清晰表达:
graph TD
A[spring-boot-starter-data-elasticsearch] --> B[Auto-configuration]
A --> C[RestHighLevelClient / Java API Client]
A --> D[ElasticsearchTemplate]
A --> E[ElasticsearchRepository Support]
B --> F[@EnableElasticsearchRepositories]
C --> G[HTTP-based Communication with ES Cluster]
D --> H[High-level Query Abstraction]
E --> I[CRUD & Custom Method Parsing]
看到没?Spring 已经悄悄把轮子造好了。你要做的,只是坐上车,握紧方向盘。
不过,如果标准配置满足不了需求呢?比如你要对接多个集群、实现多租户路由……
那就得自己动手,丰衣足食。
@Configuration
public class EsClientConfig {
@Bean(destroyMethod = "close")
public ElasticsearchClient primaryClient() throws IOException {
HttpClient transport = HttpClient.builder()
.endpoint("https://primary-es.internal:9200")
.sslConfig(new SslConfigBuilder().verifyFingerprint("your-ca-fp").build())
.build();
return new ElasticsearchClient(transport);
}
}
这里用的是全新的 Java API Client (v8+),基于 Java 17 Records 和泛型响应设计,返回结果天生类型安全,比如 SearchResponse<Product> ,再也不用手动 cast 了!
而且支持异步非阻塞调用,配合 CompletableFuture ,轻松应对高并发压力。
7. 资源释放:别让连接泄漏拖垮服务器
客户端是重量级资源,包含连接池、线程池、缓冲区……如果不妥善关闭,轻则内存泄漏,重则压垮整个 JVM。
Spring 能帮我们管理 Bean 生命周期,但也得提醒它怎么关:
@Bean(destroyMethod = "close")
public RestHighLevelClient customClient() {
return new RestHighLevelClient(RestClient.builder(HttpHost.create("http://localhost:9200")));
}
加上 destroyMethod = "close" ,就能确保在应用关闭时自动释放资源。
对于新客户端,推荐使用 try-with-resources:
try (ElasticsearchClient client = ...) {
var response = client.search(s -> s.index("products"), Product.class);
} // 自动 close()
完整的生命周期流程图如下:
stateDiagram-v2
[*] --> ApplicationContextRefresh
ApplicationContextRefresh --> CreateElasticsearchClient : @Bean + AutoConfig
CreateElasticsearchClient --> RegisterInBeanFactory
RegisterInBeanFactory --> InjectIntoServices
InjectIntoServices --> ExecuteQueries
ExecuteQueries --> ApplicationShutdown
ApplicationShutdown --> InvokeDestroyMethod : close()
InvokeDestroyMethod --> ReleaseConnectionsAndThreads
ReleaseConnectionsAndThreads --> [*]
记住一句话: 谁创建,谁负责;谁持有,谁释放 。
📦 数据模型定义:让 Java 对象“自然映射”为 ES 文档
现在轮到建模了。
Elasticsearch 是文档型数据库,不像 MySQL 那样讲究“第三范式”,它的哲学是:“扁平一点,快一点。”
但在 Spring Data Elasticsearch 中,你可以像写 POJO 一样定义实体类,框架会自动完成对象 ↔ JSON ↔ Document 的转换。
1. @Document:这是我的索引!
每个 ES 文档都属于某个索引。我们要做的第一件事,就是告诉 Spring:“这个类对应哪个索引”。
@Document(indexName = "product")
public class Product {
@Id
private String id;
private String name;
private Double price;
}
就这么简单?等等,还能更精细!
@Document(
indexName = "product",
shards = 3,
replicas = 2
)
public class Product { ... }
| 参数 | 作用 |
|---|---|
indexName |
必填,小写,可用连字符 |
shards |
主分片数,决定横向扩展能力 |
replicas |
副本数,提升容错和读性能 |
⚠️ 注意:这些设置 仅在索引首次创建时生效 。一旦索引存在,改了也没用,除非重建。
分片设计小贴士:
- 小数据(<10GB):1~3 个主分片;
- 中大型:控制单个分片大小在 10~50GB;
- 副本一般设 1~2,太多浪费资源。
2. 字段映射的艺术:Text vs Keyword
字段怎么存,直接影响搜索行为。
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String description;
@Field(type = FieldType.Keyword)
private String category;
区别在哪?
| 类型 | 分词 | 适用场景 | 能否聚合 |
|---|---|---|---|
Text |
✅ 是 | 全文检索,如商品描述 | ❌ 否 |
Keyword |
❌ 否 | 精确匹配,如状态码、邮箱 | ✅ 是 |
常见类型一览:
| 类型 | 示例 | 聚合支持 |
|---|---|---|
Date |
createdAt | ✅ |
Long / Integer |
price_int | ✅ |
Double |
rating | ✅ |
Boolean |
isHot | ✅ |
高级技巧:自定义分析器
中文搜索不用 IK 分词器?等于放弃一半战斗力。
确保 ES 节点安装了 IK 插件后,在字段上指定:
@Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart")
private String title;
ik_max_word:细粒度拆分,“人民大会堂” → “人民”、“大会堂”ik_smart:智能合并,减少冗余
3. 时间字段处理:时区陷阱千万别踩
时间字段最容易出问题的就是时区混乱。
@Field(type = FieldType.Date)
private LocalDateTime createdAt;
@Field(type = FieldType.Date, format = DateFormat.custom, pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime publishedAt;
推荐做法:
- 所有时间统一用 UTC 存储;
- 前端展示时再根据用户所在地区转换;
- 使用 Instant 或 OffsetDateTime 避免歧义。
转换流程如下:
flowchart TD
A[Java对象: LocalDateTime] --> B{序列化阶段}
B --> C[转换为Instant]
C --> D[格式化为ISO8601字符串]
D --> E[Elasticsearch存储]
E --> F{查询反序列化}
F --> G[解析为ZonedDateTime]
G --> H[还原为LocalDateTime]
H --> I[返回给业务层]
整个过程由 Jackson 和 Spring Data 协同完成,开发者无需干预,但必须保证全局时区一致。
🔍 CRUD 实战:用 Repository 写出优雅的搜索逻辑
终于到了激动人心的时刻——操作数据!
Spring Data 的精髓在于:“接口即实现”。
1. 继承 ElasticsearchRepository,立刻拥有超能力
public interface ProductRepository extends ElasticsearchRepository<Product, String> {}
就这一行,你已经拥有了:
- save() :保存或更新
- findById() :按 ID 查
- deleteById() :删除
- findAll(Pageable) :分页查询
- count() :统计总数
调用起来也特别顺滑:
@Service
public class ProductService {
@Autowired
private ProductRepository repo;
public Product create(Product p) {
return repo.save(p); // 自动生成 ID
}
public Optional<Product> getById(String id) {
return repo.findById(id);
}
public Page<Product> list(Pageable pageable) {
return repo.findAll(pageable);
}
}
底层其实是发了个 PUT 请求:
PUT /product/_doc/1
{
"name": "iPhone 15",
"price": 9999.0,
"category": "electronics"
}
简洁吧?这才是现代开发该有的样子。
2. 分页排序:大数据集下的导航艺术
面对几千条数据,全量加载肯定不行。
Spring Data 提供了 Pageable 抽象:
Pageable page = PageRequest.of(0, 10, Sort.by("price").descending());
Page<Product> result = repo.findAll(page);
生成的 DSL 是:
{
"from": 0,
"size": 10,
"sort": [{ "price": { "order": "desc" } }]
}
返回的 Page<T> 包含丰富信息:
- 当前页数据
- 总条数 totalElements
- 总页数 totalPages
- hasNext() 判断是否还有下一页
适合表格类场景。
如果是移动端无限滚动,可以用 Slice<T> ,更轻量。
3. 自定义查询:方法命名也能生成 DSL?
最惊艳的功能来了—— 方法名解析机制 。
只需定义接口方法名,Spring 就能自动生成对应的查询语句!
List<Product> findByNameContaining(String name);
List<Product> findByPriceGreaterThan(Double price);
List<Product> findByCategoryAndPriceBetween(String cat, Double min, Double max);
比如最后这个,会被翻译成:
{
"query": {
"bool": {
"must": [
{ "term": { "category": "electronics" } },
{ "range": { "price": { "gte": 1000, "lte": 5000 } } }
]
}
}
}
支持的关键字非常多:
| 关键字 | 操作 |
|---|---|
And / Or |
组合条件 |
Between |
范围查询 |
Like |
match 查询 |
In / NotIn |
集合匹配 |
IsNull |
判断空值 |
但这套机制也有局限:
- 无法处理嵌套对象(nested)
- 不支持高亮、脚本字段
- 复杂逻辑(如 (A OR B) AND NOT C )难以表达
解决方案有两个:
1. 使用 @Query 注解嵌入原生 DSL;
2. 切换到 ElasticsearchTemplate 编程式查询。
⚙️ 高级玩法:ElasticsearchTemplate 解锁全部潜力
当你发现 Repository 不够用时,是时候请出真正的“核武器”了: ElasticsearchTemplate 。
它可以让你完全掌控查询 DSL 的每一个细节。
1. 构建布尔查询:打造复杂的筛选逻辑
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
if (name != null) {
boolQuery.must(matchQuery("name", name).fuzziness(Fuzziness.AUTO));
}
if (minPrice != null) {
boolQuery.filter(rangeQuery("price").gte(minPrice));
}
NativeSearchQuery query = new NativeSearchQueryBuilder()
.withQuery(boolQuery)
.withPageable(PageRequest.of(0, 20))
.build();
SearchHits<Product> hits = template.search(query, Product.class);
亮点功能:
- fuzziness(AUTO) :模糊匹配,拼错也能搜到
- filter 子句:不参与评分,性能更高
- .keyword 后缀:避免全文分词干扰精确匹配
2. 高亮、聚合、脚本字段:前端体验升级必备
高亮显示关键词
HighlightBuilder.Field highlight = new HighlightBuilder.Field("name")
.preTags("<em class='highlight'>").postTags("</em>");
NativeSearchQuery query = new NativeSearchQueryBuilder()
.withQuery(matchQuery("description", "wireless"))
.withHighlightFields(highlight)
.build();
返回结果中可通过 getHighlightFields() 获取 HTML 片段。
聚合统计分类数量
AggregationBuilder agg = AggregationBuilders.terms("by_category").field("category.keyword");
// 执行后遍历 bucket
for (Terms.Bucket b : categoryAgg.getBuckets()) {
System.out.println(b.getKeyAsString() + ": " + b.getDocCount());
}
输出:
Electronics: 45
Clothing: 32
脚本字段动态计算折扣价
Script script = new Script(ScriptType.INLINE, "painless",
"doc['originalPrice'].value * (1 - doc['discountRate'].value)", Collections.emptyMap());
.withScriptField(new ScriptField("currentPrice", script))
查询结果直接带出 currentPrice 字段,前端免计算。
3. 原生 DSL:终极自由度
对于极端复杂的需求,可以直接构造 SearchRequest :
SearchRequest request = new SearchRequest("products");
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.query(QueryBuilders.nestedQuery(
"specifications",
QueryBuilders.boolQuery()
.must(matchQuery("specifications.key", "color"))
.must(matchQuery("specifications.value", "red")),
ScoreMode.None
));
request.source(sourceBuilder);
client.search(request, RequestOptions.DEFAULT);
适用于嵌套对象过滤、跨索引联查等场景。
🔬 测试验证:别让“假成功”骗了你
写完代码不测?等于裸奔上战场。
1. 集成测试:用 TestContainers 启真实 ES
@SpringBootTest(webEnvironment = RANDOM_PORT)
@TestPropertySource("classpath:application-test.yml")
class ProductIntegrationTest {
@Autowired
private ElasticsearchTemplate template;
@Test
void shouldFindProductAfterSave() {
Product p = new Product("Wireless Earbuds", 299.99);
template.save(p);
// 强制刷新,否则可能查不到
template.indexOps(Product.class).refresh();
long count = template.count(QueryBuilders.matchAllQuery(), Product.class);
assertThat(count).isGreaterThan(0);
}
}
💡 记住:ES 默认 1 秒刷新一次,测试中要用 refresh() 强制立即可见。
2. 断言一致性:检查数据是否真的写进去了
除了接口返回 OK,还要验证:
- 文档是否成功入库?
- 字段映射是否正确?
- 分词效果是否符合预期?
可以借助 Kibana 或 _cat/indices API 辅助排查。
🚀 生产优化:让你的搜索服务飞起来
最后一步,进入生产级调优阶段。
1. 写入优化:批量 + 调整刷新间隔
频繁单条插入?性能必然拉胯。
✅ 正确做法:
- 使用 Bulk API 批量提交;
- 临时关闭 refresh_interval;
- 设置合适的 batch size(建议 1000~5000 条 / 5~15MB);
BulkRequest bulk = new BulkRequest();
for (Product p : products) {
bulk.add(new IndexRequest("products").id(p.getId()).source(json(p), JSON));
}
bulk.setRefreshPolicy(NONE);
client.bulk(bulk, RequestOptions.DEFAULT);
2. 分片策略:合理规划才能走得长远
| 索引名 | 主分片 | 副本 | 数据量 | 场景 |
|---|---|---|---|---|
| logs-hot | 3 | 2 | 30GB/day | 实时日志 |
| products | 2 | 1 | 5GB | 商品搜索 |
| user_behavior | 5 | 1 | 100GB/mo | 行为分析 |
冷数据可用 ILM 策略自动归档。
3. 安全控制:权限隔离 + 熔断降级
- 启用 X-Pack 安全模块;
- 按角色分配索引访问权限;
- 结合 Spring Security 做租户隔离;
- 用 Hystrix/Sentinel 实现熔断,失败时返回缓存兜底数据。
4. 冷热架构 + ILM:自动化运维神器
graph TD
A[数据写入] --> B[Hot Node SSD]
B --> C{判断年龄}
C -- <7天 --> B
C -- >=7天 --> D[Warm Node SATA]
D -- >=30天 --> E[Cold Node HDD]
E -- >=90天 --> F[Delete or Snapshot]
配合 ILM 策略,全自动完成 rollover、shrink、forcemerge、delete,彻底解放运维双手。
💡 总结:为什么这个组合如此强大?
Spring Boot + Elasticsearch 的成功,不在于某一项技术多么牛,而在于它们共同构建了一种 极简而强大的开发范式 :
- 开发效率极高 :注解驱动,方法命名即查询;
- 架构灵活 :支持从简单 CRUD 到复杂 DSL 的平滑演进;
- 生产就绪 :自带监控、安全、容错机制;
- 生态完善 :Kibana、Logstash、Beats 形成完整闭环。
只要你掌握了这套组合拳,无论是做搜索、日志、监控还是推荐系统,都能游刃有余。
🎯 所以,下次当你接到“做个智能搜索”的需求时,别再犹豫了。
打开 IDE,敲下那一行依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
然后告诉老板:“明天就能演示。” 😎
简介:本文详细介绍如何在Spring Boot应用中集成并配置Elasticsearch,实现高效的搜索与实时数据分析功能。通过添加Spring Data Elasticsearch依赖、配置集群连接、定义实体类与Repository接口,开发者可快速实现CRUD操作与自定义查询。内容涵盖依赖引入、配置文件设置、实体映射、数据访问层设计、服务调用、测试策略、性能优化、异常处理及安全控制等关键环节。本Demo经过验证,适用于构建可扩展的Java搜索系统,为后续高可用与大规模数据检索应用提供基础支撑。
更多推荐


所有评论(0)