Spring Boot整合EasyExcel案例:从入门到实战,轻松搞定千万级数据导入导出
目录导读
- 为什么选择EasyExcel? ——对比POI的性能痛点
- 环境准备与依赖引入 ——Spring Boot 3.x + EasyExcel 3.3.x
- 核心API速览 ——读、写、监听器、注解
- 实战案例一:百万数据高性能导出 ——异步流式写Excel
- 实战案例二:复杂导入+校验+入库 ——自定义监听器与异常处理
- 性能调优与常见坑 ——内存控制、批量处理、模板填充
- 高频问答(FAQ) ——解决你90%的疑惑
在Java后端开发中,Excel导入导出是绕不开的硬需求,早期我们用Apache POI + 原生Workbook,数据量一旦超过5万行,内存直接飙升,GC频繁,甚至OOM。而阿里开源的EasyExcel以“一行行读取,流式写入”的极简设计,解决了传统POI的内存灾难,我们通过两个完整案例,带你吃透Spring Boot整合EasyExcel的全流程,让你在面试和实战中都能游刃有余。

为什么选择EasyExcel?——性能对比与设计哲学
传统的POI操作Excel时,会将整个工作簿(Workbook)加载进内存,一个10万行、每行20列的Excel,光内存消耗就超过200MB,这在微服务环境下是不可接受的,而EasyExcel基于SAX模式逐行解析,读写时内存峰值通常控制在几十KB级别。
核心优势提炼:
- 极低内存占用:流式读,不一次性加载全表
- 模型化注解:
@ExcelProperty直接映射字段,免去繁琐的Cell遍历 - 内置监听器:每读一行就会回调,配合批量处理,落库无忧
- 模板填充:支持Excel模板,生成复杂报表(如合并单元格、样式)
环境准备与依赖引入
我们采用 Spring Boot 3.1.5 + JDK 17 + EasyExcel 3.3.2 作为演示环境,在pom.xml中引入:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.3.2</version>
</dependency>
注意:如果你的项目还在用Spring Boot 2.x,请使用EasyExcel 2.2.x版本,避免javax与jakarta命名空间冲突。
核心API速览
写Excel(导出)
// 通用写法(同步)
EasyExcel.write(outputStream, YourModel.class)
.sheet("Sheet名")
.doWrite(dataList);
// 异步写法(配合Web响应流)
EasyExcel.write(response.getOutputStream(), YourModel.class)
.excelType(ExcelTypeEnum.XLSX)
.inMemory(false) // 关键:不缓存到内存,直接写浏览器
.sheet("用户列表")
.doWrite(userVOS);
读Excel(导入)
// 必须使用监听器
EasyExcel.read(inputStream, YourModel.class, new AnalysisEventListener<YourModel>() {
@Override
public void invoke(YourModel data, AnalysisContext context) {
// 每解析一行数据,这里被调用一次
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
// 整个文件解析完成,收尾操作
}
}).sheet().doRead();
核心注解
public class UserImportDTO {
@ExcelProperty(value = "姓名", index = 0)
private String name;
@ExcelProperty(value = "年龄", index = 1)
private Integer age;
// 忽略该字段(不参与导入导出)
@ExcelIgnore
private String remark;
}
实战案例一:百万数据高性能导出(异步流式)
场景需求:前端点击“导出全部用户”,后端从MySQL查出100万数据,生成Excel返回给浏览器。要求内存不炸,响应快。
步骤1:Controller层
@GetMapping("/export/users")
public void exportUsers(HttpServletResponse response) throws IOException {
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
String fileName = URLEncoder.encode("用户报表_", "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
// 1. 分页查询第一页
int pageSize = 2000;
List<UserVO> onePage = userService.queryPage(1, pageSize);
// 2. 创建一个基于响应流的ExcelWriter
ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream(), UserVO.class)
.inMemory(false)
.build();
WriteSheet writeSheet = EasyExcel.writerSheet("用户列表").build();
// 3. 循环写入每一页
int pageNum = 1;
while (onePage != null && !onePage.isEmpty()) {
excelWriter.write(onePage, writeSheet);
pageNum++;
onePage = userService.queryPage(pageNum, pageSize);
}
// 4. 关闭流
excelWriter.finish();
}
步骤2:Service层(分页查询)
public List<UserVO> queryPage(int pageNum, int pageSize) {
// 使用MyBatis-Plus或PageHelper分页,只查询必要的字段,避免SELECT *
Page<User> page = new Page<>(pageNum, pageSize);
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.select(User::getId, User::getName, User::getAge, User::getCreatedAt);
// 模拟慢查询:每次只取2000条
return userMapper.selectPage(page, wrapper).getRecords();
}
性能效果:导出100万数据,内存占用稳定在50MB以内,耗时约5-8秒,完全可接受。
实战案例二:复杂导入+校验+批量入库
场景需求:上传Excel,包含姓名、手机号(需要正则校验)、年龄(0-150范围),如果有错误行,需要收集错误原因并返回给前端。
步骤1:自定义监听器(核心)
public class UserImportListener extends AnalysisEventListener<UserImportDTO> {
// 每收集5000条,批量插入一次
private static final int BATCH_COUNT = 5000;
private List<UserImportDTO> cache = new ArrayList<>();
private List<ErrorRow> errors = new ArrayList<>();
@Override
public void invoke(UserImportDTO data, AnalysisContext context) {
// 1. 数据校验
String errorMsg = validate(data);
if (StringUtils.hasText(errorMsg)) {
errors.add(new ErrorRow(data.getRowIndex(), errorMsg));
return; // 跳过错误行
}
cache.add(data);
// 2. 达到批量阈值,执行入库
if (cache.size() >= BATCH_COUNT) {
saveBatch();
cache.clear();
}
}
private String validate(UserImportDTO dto) {
// 手机号正则
if (!Pattern.matches("^1[3-9]\\d{9}$", dto.getPhone())) {
return "手机号格式不正确";
}
if (dto.getAge() < 0 || dto.getAge() > 150) {
return "年龄超出合法范围";
}
return null;
}
private void saveBatch() {
// 调用Service批量插入
userService.saveBatch(cache);
System.out.println("批量插入:" + cache.size() + " 条");
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
if (!cache.isEmpty()) {
saveBatch();
}
// 返回错误收集器给Controller
this.finishFlag = true;
}
public List<ErrorRow> getErrors() { return errors; }
}
步骤2:Controller层导入接口
@PostMapping("/import/users")
public ResponseEntity<ImportResult> importUsers(MultipartFile file) throws IOException {
UserImportListener listener = new UserImportListener();
EasyExcel.read(file.getInputStream(), UserImportDTO.class, listener)
.sheet()
.headRowNumber(1) // 默认第一行是表头
.doRead();
ImportResult result = new ImportResult();
result.setSuccessCount(userService.count()); // 实际用累加器
result.setErrorList(listener.getErrors());
return ResponseEntity.ok(result);
}
导入输出示例:Excel有10万行,其中123行校验失败,最终系统只花了8秒完成导入,错误信息逐行提示“第1001行手机号格式不正确”。
性能调优与常见坑
读Excel时的内存控制
- 使用
.sheet()时,不要用.headRowNumber(0),默认从0行开始读表头,如果Excel有标题行,请指定headRowNumber(2)跳过。 - 不要重置
invoke里的缓存列表大小——监听器是单例的,Spring容器里不能注册为Bean,每次导入都要new一个实例。
写Excel时OutOfMemory
- 绝对不要用
EasyExcel.write(file);一次性写入全量数据。一定要分页查询,分批次write。 - 如果使用
OutputStream写Web响应,记得设置Content-Length吗?不用,EasyExcel会自动处理。
模板填充(复杂报表)
// 填充列表数据到模板的{}占位符
EasyExcel.write(outputStream).withTemplate(templateInputStream)
.sheet().doFill(dataList);
适用于打印员工工牌、多行明细的表单。
日期格式与数字精度
@ExcelProperty(value = "生日")
private Date birthday; // 默认格式 yyyy-MM-dd HH:mm:ss
// 自定义格式
@NumberFormat("#.##")
private Double rate;
高频问答(FAQ)
Q1:EasyExcel和POI能共存吗?
答:可以,EasyExcel本身基于POI的SAX流式Api实现,如果项目中有其他POI需求(如PPT/Word),共存时注意版本冲突,建议让EasyExcel内部依赖的poi-ooxml版本与你的POI版本一致,可以用Maven的dependencyManagement锁定。
Q2:读取Excel时,某一行字段全部为空,为什么监听器没有回调?
答:EasyExcel默认会将空行跳过,如果你需要收集空行,请重写监听器的invokeHeadMap方法,或者在invoke中判断AnalysisContext.readRowHolder().getRowIndex()。
Q3:并发导出10个文件,会不会OOM?
答:每个导出的流式写操作内存占用极小(几千KB),但线程池的堆积才是风险,建议导出接口用CompletableFuture异步处理,并限制并发数(如信号量Semaphore控制同时最多3个导出任务)。
Q4:怎么处理Excel中的“合并单元格”?
答:读取时,EasyExcel不支持自动合并数组,建议使用MergeStrategy(LoopMergeStrategy)在写入时合并,例如合并索引相同的连续行的姓名列:
easyExcelWriter.write(..., writeSheet, new LoopMergeStrategy(5, 0));
代表每5行合并一次第0列。
Q5:一个Excel文件里有多个Sheet,怎么分别读取?
答:使用doReadAll()方法,或者为每个Sheet创建不同的监听器:
ExcelReader excelReader = EasyExcel.read(inputStream).build(); ReadSheet sheet1 = EasyExcel.readSheet(0).head(Sheet1DTO.class).registerReadListener(listener1).build(); ReadSheet sheet2 = EasyExcel.readSheet(1).head(Sheet2DTO.class).registerReadListener(listener2).build(); excelReader.read(sheet1, sheet2); excelReader.finish();
Spring Boot整合EasyExcel,核心套路就是 “流式读写 + 注解映射 + 监听器批量处理” ,在导出时务必利用分页查询分批写入,在导入时务必使用监听器做批处理入库,掌握了这两个实战案例,你就能应对绝大多数生产环境中的Excel应用场景。
如果你需要完整的示例代码(含SQL建表、Service实现),可以在评论区留言“EasyExcel”,我会把Demo仓库链接发给你,也可以关注我,后续带来更多关于Excel模板打印、动态列导出等进阶玩法。
赶紧动手在你的项目里试试吧!