Spring Boot 2.5.0 集成 Elasticsearch 7.12.0 实现 CRUD 完整指南(Windows 环境)
目录
1. Elasticsearch 简介
Elasticsearch(简称 ES)是一款基于 Lucene 构建的分布式、高可用、实时的全文搜索引擎,同时也是 Elastic Stack(ELK Stack:Elasticsearch、Logstash、Kibana)的核心组件。
核心特性
全文检索:支持对文本内容进行分词、模糊匹配、精准查询,适用于日志检索、商品搜索等场景;
分布式架构:自动分片存储数据,支持水平扩展,可应对海量数据存储与高并发查询;
实时响应:数据写入后近实时可查(默认 1 秒内),查询延迟低,满足实时业务需求;
多数据类型支持:除文本外,还支持数值、日期、地理坐标等多种数据类型,适配复杂业务场景;
RESTful API:通过 HTTP 协议即可操作 ES,支持 JSON 格式交互,集成成本低。
适用场景
电商平台商品搜索(如按名称、描述模糊查询);
日志 / 监控数据存储与分析(如收集系统日志并快速检索异常信息);
企业内部文档检索(如知识库、文档管理系统);
实时数据分析(如用户行为数据实时统计)。
2. 环境准备
在开始集成前,确保本地环境满足以下条件:
JDK 版本:JDK 8 或 JDK 11(ES 7.12.0 依赖此版本范围,避免版本不兼容);
Spring Boot 版本:2.5.0(项目已指定 parent,无需额外修改版本);
Elasticsearch 版本:7.12.0(Windows 压缩包版,与 Spring Boot 2.5.0 兼容);
开发工具:IntelliJ IDEA(或 Eclipse,推荐 IDEA 方便代码管理);
测试工具:Postman(或 Swagger、curl,用于测试 API 接口)。
3. Elasticsearch 安装与启动
3.1 下载与解压
直接安装 7.12.0 版本:https://www.elastic.co/cn/downloads/past-releases/elasticsearch-7-12-0
访问 Elasticsearch 历史版本下载页:https://www.elastic.co/cn/downloads/past-releases#elasticsearch;
找到 7.12.0 版本,选择 Windows 系统的压缩包(
elasticsearch-7.12.0-windows-x86_64.zip)下载;解压到 无中文、无空格 的目录(例如
D:\elasticsearch-7.12.0,路径含特殊字符会导致启动失败)。
3.2 启动 Elasticsearch 服务
进入解压目录下的
bin文件夹(完整路径:D:\elasticsearch-7.12.0\bin);双击
elasticsearch.bat文件,自动弹出命令行窗口(不要关闭此窗口,关闭即停止服务);等待启动完成:当命令行输出
started字样,且无报错信息时,说明服务启动成功(首次启动约 10-30 秒)。
3.3 验证启动状态
打开浏览器,访问
http://localhost:9200(ES 默认 HTTP 端口为 9200);若返回以下 JSON 响应,证明 ES 服务正常运行:
{
"name" : "DESKTOP-XXXXXX", // 你的电脑名称
"cluster_name" : "elasticsearch", // 默认集群名称
"cluster_uuid" : "XXXXXXXXXXXXXXXXXXXXX",
"version" : {
"number" : "7.12.0", // ES 版本,需与下载一致
"build_flavor" : "default",
"build_type" : "zip",
"build_hash" : "78722783c38caa25a709d81e9ec61e65bde69113",
"build_date" : "2021-03-18T06:17:15.410153305Z",
"build_snapshot" : false,
"lucene_version" : "8.8.0",
"minimum_wire_compatibility_version" : "6.8.0",
"minimum_index_compatibility_version" : "6.0.0-beta1"
},
"tagline" : "You Know, for Search"
}
4. Spring Boot 项目配置
4.1 添加依赖(pom.xml)
在项目的 pom.xml 中,补充 Elasticsearch 及相关依赖(已有 parent 无需额外指定版本):
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.0</version>
<relativePath/>
</parent>
<!-- Spring Boot Data Elasticsearch:提供 ES 数据访问能力 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<!-- Jackson:处理 JSON 格式转换(实体类与 ES 文档交互) -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- 可选:Swagger3:生成 API 文档,方便网页端测试 -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>
4.2 配置 ES 连接(application.properties)
在 src/main/resources/application.properties 中,添加 ES 连接参数:
# ES 服务地址(默认 9200 端口,多个地址用逗号分隔)
spring.elasticsearch.rest.uris=http://localhost:9200
# 连接超时时间(1秒,避免长时间等待)
spring.elasticsearch.rest.connection-timeout=1s
# 读取超时时间(3秒,适配大数据量查询)
spring.elasticsearch.rest.read-timeout=3s
# 可选:日志配置(调试时开启,查看 ES 交互细节)
logging.level.org.springframework.data.elasticsearch=DEBUG
logging.level.org.elasticsearch=DEBUG
5. CRUD 核心代码实现
以「书籍(Book)」为业务模型,实现 ES 文档的 Create(创建)、Read(查询)、Update(更新)、Delete(删除) 操作,代码遵循「实体类 → Repository → Service → Controller」分层设计。
5.1 实体类(Book.java)
定义 ES 文档结构,通过注解映射索引、字段类型及分词规则:
package com.example.demo.model;
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:指定 ES 索引名称(相当于数据库的“表”)
* indexName:索引名,建议小写;shards:分片数(默认5);replicas:副本数(默认1)
*/
@Document(indexName = "books", shards = 1, replicas = 0)
public class Book
{
// @Id:ES 文档的唯一标识(相当于数据库的“主键”)
@Id
private String id;
/**
* @Field:配置字段属性
* type:字段类型(Text 支持分词,Keyword 不支持分词)
* analyzer:分词器(ik_max_word 为 IK 分词器细粒度分词,适合全文检索)
* searchAnalyzer:查询时使用的分词器(与 analyzer 一致即可)
*/
@Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_max_word")
private String title;
// 书籍标题
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String author;
// 书籍作者
// 数值类型:无需分词,直接存储
@Field(type = FieldType.Integer)
private Integer price;
// 书籍价格
// 日期类型:指定格式,避免 ES 自动转换
@Field(type = FieldType.Date, format = {
}, pattern = "yyyy-MM-dd")
private String publishDate;
// 出版日期
// 无参构造:Spring Data 要求必须存在
public Book() {
}
// 有参构造:快速创建 Book 对象
public Book(String title, String author, Integer price, String publishDate) {
this.title = title;
this.author = author;
this.price = price;
this.publishDate = publishDate;
}
// Getter 和 Setter(必须生成,否则无法注入数据)
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public Integer getPrice() {
return price;
}
public void setPrice(Integer price) {
this.price = price;
}
public String getPublishDate() {
return publishDate;
}
public void setPublishDate(String publishDate) {
this.publishDate = publishDate;
}
// toString:方便打印日志或调试时查看对象信息
@Override
public String toString() {
return "Book{" +
"id='" + id + '\'' +
", title='" + title + '\'' +
", author='" + author + '\'' +
", price=" + price +
", publishDate='" + publishDate + '\'' +
'}';
}
}
说明:若需使用 IK 分词器,需先在 ES 中安装(步骤见 7.1 常见问题)。
5.2 Repository 接口(BookRepository.java)
继承 ElasticsearchRepository,Spring Data 会自动实现 CRUD 基础方法,无需手动编写查询逻辑:
package com.example.demo.repository;
import com.example.demo.model.Book;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
/**
* @Repository:标记为数据访问层组件,让 Spring 自动扫描注入
* ElasticsearchRepository<实体类, 主键类型>:提供基础 CRUD 方法(save、findById、delete 等)
*/
@Repository
public interface BookRepository extends ElasticsearchRepository<
Book, String> {
// 自定义查询方法:根据作者查询书籍(Spring Data 自动解析方法名生成 ES 查询)
List<
Book> findByAuthor(String author);
// 自定义查询方法:根据标题包含关键字查询(Containing 相当于“LIKE %关键字%”)
List<
Book> findByTitleContaining(String keyword);
// 自定义查询方法:根据价格范围查询(Between 对应 ES 的范围查询)
List<
Book> findByPriceBetween(Integer minPrice, Integer maxPrice);
}
5.3 Service 层(BookService.java)
封装业务逻辑,调用 Repository 实现 CRUD 操作,解耦 Controller 与数据访问层:
package com.example.demo.service;
import com.example.demo.model.Book;
import com.example.demo.repository.BookRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
/**
* @Service:标记为业务逻辑层组件,Spring 自动扫描注入
*/
@Service
public class BookService
{
// 注入 Repository 依赖(Spring 自动完成,无需手动 new)
@Autowired
private BookRepository bookRepository;
/**
* 1. Create/Update:创建或更新文档
* 逻辑:若 id 存在则更新,不存在则创建新文档
*/
public Book saveBook(Book book) {
return bookRepository.save(book);
}
/**
* 2. Read:根据 id 查询单个文档
* 返回 Optional<Book>:避免空指针,需通过 isPresent() 判断是否存在
*/
public Optional<
Book> getBookById(String id) {
return bookRepository.findById(id);
}
/**
* 3. Read:查询所有文档
* 返回 Iterable<Book>:支持迭代遍历所有文档
*/
public Iterable<
Book> getAllBooks() {
return bookRepository.findAll();
}
/**
* 4. Read:根据作者查询文档
*/
public List<
Book> getBooksByAuthor(String author) {
return bookRepository.findByAuthor(author);
}
/**
* 5. Read:根据标题关键字搜索文档
*/
public List<
Book> searchBooksByTitle(String keyword) {
return bookRepository.findByTitleContaining(keyword);
}
/**
* 6. Delete:根据 id 删除文档
*/
public void deleteBookById(String id) {
bookRepository.deleteById(id);
}
}
5.4 Controller 层(BookController.java)
提供 RESTful API 接口,供外部调用(如 Postman、前端页面),接收请求并返回响应:
package com.example.demo.controller;
import com.example.demo.model.Book;
import com.example.demo.service.BookService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Optional;
/**
* @RestController:标记为 REST 接口控制器(返回 JSON 数据,而非页面)
* @RequestMapping:指定接口基础路径(所有接口前缀为 /api/books)
*/
@RestController
@RequestMapping("/api/books")
public class BookController
{
@Autowired
private BookService bookService;
/**
* 1. 创建文档(POST 请求)
* @RequestBody:将请求体中的 JSON 数据转换为 Book 对象
* 响应:201 Created(创建成功)+ 新文档数据
*/
@PostMapping
public ResponseEntity<
Book> createBook(@RequestBody Book book) {
Book savedBook = bookService.saveBook(book);
return new ResponseEntity<
>(savedBook, HttpStatus.CREATED);
}
/**
* 2. 根据 id 查询文档(GET 请求)
* @PathVariable:从 URL 路径中获取 id 参数(如 /api/books/123 中的 123)
* 响应:200 OK(存在)/ 404 Not Found(不存在)
*/
@GetMapping("/{id}")
public ResponseEntity<
Book> getBookById(@PathVariable String id) {
Optional<
Book> book = bookService.getBookById(id);
// 三元表达式:存在则返回 200+数据,不存在返回 404
return book.isPresent() ? ResponseEntity.ok(book.get()) : ResponseEntity.notFound().build();
}
/**
* 3. 查询所有文档(GET 请求)
* 响应:200 OK + 所有文档列表
*/
@GetMapping
public ResponseEntity<
Iterable<
Book>
> getAllBooks() {
return ResponseEntity.ok(bookService.getAllBooks());
}
/**
* 4. 根据作者查询文档(GET 请求)
* @PathVariable:从 URL 路径中获取 author 参数
* 响应:200 OK + 符合条件的文档列表
*/
@GetMapping("/author/{author}")
public ResponseEntity<
List<
Book>
> getBooksByAuthor(@PathVariable String author) {
return ResponseEntity.ok(bookService.getBooksByAuthor(author));
}
/**
* 5. 根据标题关键字搜索(GET 请求)
* 响应:200 OK + 符合条件的文档列表
*/
@GetMapping("/search/title/{keyword}")
public ResponseEntity<
List<
Book>
> searchBooksByTitle(@PathVariable String keyword) {
return ResponseEntity.ok(bookService.searchBooksByTitle(keyword));
}
/**
* 6. 更新文档(PUT 请求)
* 逻辑:先查询 id 是否存在,存在则更新,不存在返回 404
* 响应:200 OK(更新成功)/ 404 Not Found(文档不存在)
*/
@PutMapping("/{id}")
public ResponseEntity<
Book> updateBook(@PathVariable String id, @RequestBody Book book) {
// 1. 先查询文档是否存在
Optional<
Book> existingBook = bookService.getBookById(id);
if (existingBook.isPresent()) {
// 2. 存在则设置 id(避免更新时生成新文档),再执行更新
book.setId(id);
Book updatedBook = bookService.saveBook(book);
return ResponseEntity.ok(updatedBook);
} else {
// 3. 不存在则返回 404
return ResponseEntity.notFound().build();
}
}
/**
* 7. 删除文档(DELETE 请求)
* 逻辑:先查询 id 是否存在,存在则删除,不存在返回 404
* 响应:204 No Content(删除成功,无返回内容)/ 404 Not Found(文档不存在)
*/
@DeleteMapping("/{id}")
public ResponseEntity<
Void> deleteBook(@PathVariable String id) {
if (bookService.getBookById(id).isPresent()) {
bookService.deleteBookById(id);
// 204 状态码:表示请求成功但无响应体
return new ResponseEntity<
>(HttpStatus.NO_CONTENT);
} else {
return ResponseEntity.notFound().build();
}
}
}
6. CRUD 测试方法
测试前需确保两个服务正常运行:
Elasticsearch 服务(
http://localhost:9200可访问);Spring Boot 应用(默认端口 8080,无端口冲突)。
6.1 方法 1:使用 Postman 测试(推荐)
打开 Postman,按照以下用例测试所有 CRUD 接口,每个接口的请求参数和预期结果如下:
| 接口功能 | 请求方式 | 请求 URL | 请求体(JSON) | 预期响应状态码 | 预期响应内容 |
|---|---|---|---|---|---|
| 创建书籍 | POST | http://localhost:8080/api/books | {"title":"Spring Boot实战","author":"张三","price":59,"publishDate":"2020-01-15"} | 201 Created | 返回创建的书籍完整信息(含自动生成的 id) |
| 根据 id 查询书籍 | GET | http://localhost:8080/api/books/1 | 无(将 1 替换为创建时返回的 id) | 200 OK(存在) | 返回对应 id 的书籍信息 |
| 查询所有书籍 | GET | http://localhost:8080/api/books | 无 | 200 OK | 返回所有已创建的书籍列表 |
| 根据作者查询 | GET | http://localhost:8080/api/books/author/张三 | 无 | 200 OK | 返回所有 “张三” 创作的书籍 |
| 标题关键字搜索 | GET | http://localhost:8080/api/books/search/title/Spring | 无 | 200 OK | 返回标题包含 “Spring” 的书籍 |
| 更新书籍 | PUT | http://localhost:8080/api/books/1 | {"title":"Spring Boot实战(第二版)","author":"张三","price":69,"publishDate":"2021-05-20"} | 200 OK(存在) | 返回更新后的书籍信息 |
| 删除书籍 | DELETE | http://localhost:8080/api/books/1 | 无 | 204 No Content(存在) | 无响应体,仅返回状态码 |
注意:若测试 “根据 id 查询 / 更新 / 删除” 时,id 不存在,响应状态码会返回 404 Not Found,属于正常现象。
6.2 方法 2:使用 Swagger 测试(网页端)
若已添加 Swagger 依赖和配置,可通过网页直接测试接口,步骤如下:
启动 Spring Boot 应用后,访问 Swagger 文档地址:
http://localhost:8080/swagger-ui/index.html;页面会显示所有
/api/books前缀的接口,点击任意接口名称(如POST /api/books)展开详情;点击「Try it out」按钮,输入请求参数(如创建书籍时的 JSON 数据);
点击「Execute」按钮执行请求,下方会显示响应状态码和响应内容,无需手动拼接 URL。
6.3 方法 3:使用 JUnit 单元测试(自动化验证)
编写单元测试类,直接测试 Service 层逻辑,无需依赖外部工具,步骤如下:
6.3.1 测试类代码(BookServiceTest.java)
package com.example.demo.service;
import com.example.demo.model.Book;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
// 启动 Spring Boot 上下文,用于注入依赖
@SpringBootTest
public class BookServiceTest
{
@Autowired
private BookService bookService;
// 测试用的书籍 id(用于后续查询、更新、删除)
private String testBookId;
// 测试前初始化:创建一条测试数据
@BeforeEach
void setUp() {
Book testBook = new Book("JUnit 测试书籍", "测试作者", 39, "2024-01-01");
Book savedBook = bookService.saveBook(testBook);
testBookId = savedBook.getId();
// 保存生成的 id
}
// 测试后清理:删除测试数据,避免影响其他测试
@AfterEach
void tearDown() {
bookService.deleteBookById(testBookId);
}
// 测试创建文档
@Test
void testSaveBook() {
Book newBook = new Book("新测试书籍", "新作者", 49, "2024-02-02");
Book savedBook = bookService.saveBook(newBook);
assertNotNull(savedBook.getId());
// 验证 id 非空(创建成功)
assertEquals("新测试书籍", savedBook.getTitle());
// 验证标题正确
// 清理测试数据
bookService.deleteBookById(savedBook.getId());
}
// 测试根据 id 查询文档
@Test
void testGetBookById() {
Optional<
Book> foundBook = bookService.getBookById(testBookId);
assertTrue(foundBook.isPresent());
// 验证文档存在
assertEquals("JUnit 测试书籍", foundBook.get().getTitle());
// 验证标题匹配
}
// 测试查询所有文档
@Test
void testGetAllBooks() {
Iterable<
Book> allBooks = bookService.getAllBooks();
assertTrue(allBooks.iterator().hasNext());
// 验证存在至少一条数据(即 setUp 中创建的测试数据)
}
// 测试根据作者查询文档
@Test
void testGetBooksByAuthor() {
List<
Book> booksByAuthor = bookService.getBooksByAuthor("测试作者");
assertFalse(booksByAuthor.isEmpty());
// 验证查询结果非空
// 验证所有结果的作者都是“测试作者”
for (Book book : booksByAuthor) {
assertEquals("测试作者", book.getAuthor());
}
}
// 测试根据标题关键字搜索
@Test
void testSearchBooksByTitle() {
List<
Book> searchedBooks = bookService.searchBooksByTitle("测试");
assertFalse(searchedBooks.isEmpty());
// 验证搜索结果非空
// 验证所有结果的标题包含“测试”
for (Book book : searchedBooks) {
assertTrue(book.getTitle().contains("测试"));
}
}
// 测试更新文档
@Test
void testUpdateBook() {
// 1. 查询测试数据
Optional<
Book> bookToUpdate = bookService.getBookById(testBookId);
assertTrue(bookToUpdate.isPresent());
// 2. 修改价格
Book updatedBook = bookToUpdate.get();
updatedBook.setPrice(59);
bookService.saveBook(updatedBook);
// 3. 验证更新结果
Optional<
Book> result = bookService.getBookById(testBookId);
assertEquals(59, result.get().getPrice());
// 价格已从 39 改为 59
}
// 测试删除文档
@Test
void testDeleteBookById() {
// 1. 删除测试数据
bookService.deleteBookById(testBookId);
// 2. 验证删除结果
Optional<
Book> deletedBook = bookService.getBookById(testBookId);
assertFalse(deletedBook.isPresent());
// 验证文档已不存在
}
}
6.3.2 运行测试
在 IDEA 中打开
BookServiceTest.java;右键点击类名,选择「Run ‘BookServiceTest’」;
等待测试完成,若所有测试方法前显示绿色对勾,说明 CRUD 逻辑全部正常。
7. 常见问题排查
在集成或测试过程中,可能会遇到以下问题,可按对应的解决方案排查:
7.1 问题 1:启动 Spring Boot 应用时,报错 “找不到 IK 分词器”
报错信息:Elasticsearch exception [type=illegal_argument_exception, reason=failed to find analyzer [ik_max_word]]
原因:实体类中使用了 analyzer = "ik_max_word"(IK 分词器),但 ES 未安装该插件。
解决方案:
下载 IK 分词器插件(需与 ES 版本一致,即 7.12.0):
下载地址:https://github.com/medcl/elasticsearch-analysis-ik/releases/tag/v7.12.0,选择
elasticsearch-analysis-ik-7.12.0.zip;在 ES 安装目录下,新建
plugins/ik文件夹(路径:D:\elasticsearch-7.12.0\plugins\ik);将下载的压缩包解压到
ik文件夹中;重启 Elasticsearch 服务,IK 分词器会自动加载。
7.2 问题 2:访问 http://localhost:9200 时,提示 “连接拒绝”
原因:ES 服务未启动,或端口被占用。
解决方案:
检查 ES 启动窗口是否正常运行(未关闭且无报错);
若 ES 已启动,打开命令提示符(CMD),执行
netstat -ano | findstr "9200",查看 9200 端口是否被其他进程占用;若端口被占用,结束占用进程(通过任务管理器,根据 PID 找到对应进程),或修改 ES 端口(在
config/elasticsearch.yml中添加http.port: 9201,重启 ES)。
7.3 问题 3:测试 “创建书籍” 时,响应状态码 500,报错 “连接超时”
报错信息:Elasticsearch exception [type=connect_timeout_exception, reason=connect timed out]
原因:Spring Boot 应用无法连接到 ES 服务。
解决方案:
确认 ES 服务已启动,且
http://localhost:9200可访问;检查
application.properties中的spring.elasticsearch.rest.uris配置是否正确(是否为http://localhost:9200,无多余空格或符号);若 ES 端口已修改(如改为 9201),需同步更新该配置为
http://localhost:9201。
7.4 问题 4:单元测试时,报错 “无法注入 BookService”
报错信息:No qualifying bean of type 'com.example.demo.service.BookService' available
原因:Spring 未扫描到 Service 组件,可能是包路径配置错误。
解决方案:
检查 Spring Boot 启动类(如
DemoApplication.java)是否添加了@SpringBootApplication注解;确保启动类所在的包是所有业务类(Service、Controller、Repository)的父包,例如:
启动类路径:
com.example.demoService 路径:
com.example.demo.service(子包,可被扫描);
- 若包路径不满足父子关系,可在启动类上添加
@ComponentScan(basePackages = "com.example.demo"),指定扫描的包范围。
浙公网安备 33010602011771号