Spring Boot 2.5.0 集成 Elasticsearch 7.12.0 实现 CRUD 完整指南(Windows 环境)
Spring Boot 2.5.0 集成 Elasticsearch 7.12.0 实现 CRUD 完整指南(Windows 环境)
目录
-
Elasticsearch 简介
-
环境准备
-
Elasticsearch 安装与启动
-
Spring Boot 项目配置
-
CRUD 核心代码实现
-
CRUD 测试方法
-
常见问题排查
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 文档的唯一标识(相当于数据库的“主键”)@Idprivate 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:方便打印日志或调试时查看对象信息@Overridepublic 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)@Autowiredprivate 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 {@Autowiredprivate BookService bookService;/*** 1. 创建文档(POST 请求)* @RequestBody:将请求体中的 JSON 数据转换为 Book 对象* 响应:201 Created(创建成功)+ 新文档数据*/@PostMappingpublic 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+数据,不存在返回 404return book.isPresent() ? ResponseEntity.ok(book.get()) : ResponseEntity.notFound().build();}/*** 3. 查询所有文档(GET 请求)* 响应:200 OK + 所有文档列表*/@GetMappingpublic 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. 不存在则返回 404return 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 {@Autowiredprivate BookService bookService;// 测试用的书籍 id(用于后续查询、更新、删除)private String testBookId;// 测试前初始化:创建一条测试数据@BeforeEachvoid setUp() {Book testBook = new Book("JUnit 测试书籍", "测试作者", 39, "2024-01-01");Book savedBook = bookService.saveBook(testBook);testBookId = savedBook.getId(); // 保存生成的 id}// 测试后清理:删除测试数据,避免影响其他测试@AfterEachvoid tearDown() {bookService.deleteBookById(testBookId);}// 测试创建文档@Testvoid 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 查询文档@Testvoid testGetBookById() {Optional<Book> foundBook = bookService.getBookById(testBookId);assertTrue(foundBook.isPresent()); // 验证文档存在assertEquals("JUnit 测试书籍", foundBook.get().getTitle()); // 验证标题匹配}// 测试查询所有文档@Testvoid testGetAllBooks() {Iterable<Book> allBooks = bookService.getAllBooks();assertTrue(allBooks.iterator().hasNext()); // 验证存在至少一条数据(即 setUp 中创建的测试数据)}// 测试根据作者查询文档@Testvoid testGetBooksByAuthor() {List<Book> booksByAuthor = bookService.getBooksByAuthor("测试作者");assertFalse(booksByAuthor.isEmpty()); // 验证查询结果非空// 验证所有结果的作者都是“测试作者”for (Book book : booksByAuthor) {assertEquals("测试作者", book.getAuthor());}}// 测试根据标题关键字搜索@Testvoid testSearchBooksByTitle() {List<Book> searchedBooks = bookService.searchBooksByTitle("测试");assertFalse(searchedBooks.isEmpty()); // 验证搜索结果非空// 验证所有结果的标题包含“测试”for (Book book : searchedBooks) {assertTrue(book.getTitle().contains("测试"));}}// 测试更新文档@Testvoid 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}// 测试删除文档@Testvoid 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.demo
-
Service 路径:
com.example.demo.service
(子包,可被扫描);
- 若包路径不满足父子关系,可在启动类上添加
@ComponentScan(basePackages = "com.example.demo")
,指定扫描的包范围。