Spring Boot 系列:集成 EasyExcel 实现百万级数据导入导出实战

本文基于开源项目 springboot-easyexcel-batch 进行解析与扩展,手把手教你如何在 Spring Boot 2.2.1 中集成 Alibaba EasyExcel,轻松实现 百万级数据的导入与导出


目录

  1. 项目结构概览
  2. 核心依赖
  3. 百万级导出实战
  4. 百万级导入实战
  5. 性能优化技巧
  6. 常见问题 & 解决方案
  7. 总结

项目结构概览

springboot-easyexcel-batch
├── src/main/java/com/example/easyexcel
│ ├── controller/ # 导入导出接口
│ ├── listener/ # 导入监听器
│ ├── model/ # 实体类
│ ├── service/ # 业务逻辑
│ └── Application.java # 启动类
└── src/main/resources
├── application.yml # 线程池配置
└── templates/ # 前端demo

核心依赖

<!-- Spring Boot 2.2.1 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.1.RELEASE</version>
</parent> <!-- EasyExcel 2.2.11(稳定版) -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>2.2.11</version>
</dependency>

百万级导出实战

1️⃣ 场景

需求 数据量 策略
导出用户表 100万+ 分Sheet + 分批查询 + 边查边写

2️⃣ 核心代码

package com.example.easyexcel.service;

import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.example.easyexcel.model.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service; import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture; @Service
@Slf4j
public class ExcelExportService { private final ThreadPoolTaskExecutor excelExecutor;
private final UserService userService; // 每个Sheet的数据量
private static final int DATA_PER_SHEET = 100000; // 每次查询的数据量
private static final int QUERY_BATCH_SIZE = 10000; public ExcelExportService(ThreadPoolTaskExecutor excelExecutor, UserService userService) {
this.excelExecutor = excelExecutor;
this.userService = userService;
} /**
* 导出百万级用户数据(优化内存版本)
*/
public void exportMillionUsers(HttpServletResponse response, long totalCount) 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");
response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
response.setHeader("Pragma", "no-cache");
response.setDateHeader("Expires", 0); // 计算总Sheet数
int sheetCount = (int) (totalCount / DATA_PER_SHEET + (totalCount % DATA_PER_SHEET > 0 ? 1 : 0));
log.info("需要生成的Sheet总数:{}", sheetCount); try (OutputStream os = response.getOutputStream()) {
// 创建ExcelWriter,直接写入响应输出流
ExcelWriter excelWriter = EasyExcel.write(os, User.class).build(); // 用于保证Sheet写入顺序的前一个Future
CompletableFuture<Void> previousFuture = CompletableFuture.completedFuture(null); for (int sheetNo = 0; sheetNo < sheetCount; sheetNo++) {
final int currentSheetNo = sheetNo;
long start = currentSheetNo * (long) DATA_PER_SHEET;
long end = Math.min((currentSheetNo + 1) * (long) DATA_PER_SHEET, totalCount); // 每个Sheet的处理依赖于前一个Sheet完成,保证顺序
previousFuture = previousFuture.thenRunAsync(() -> {
try {
log.info("开始处理Sheet {} 的数据({} - {})", currentSheetNo, start, end);
writeSheetData(excelWriter, currentSheetNo, start, end);
log.info("完成处理Sheet {} 的数据", currentSheetNo);
} catch (Exception e) {
log.error("处理Sheet {} 数据失败", currentSheetNo, e);
throw new RuntimeException("处理Sheet " + currentSheetNo + " 数据失败", e);
}
}, excelExecutor);
} // 等待所有Sheet处理完成
previousFuture.join(); // 完成写入
excelWriter.finish();
log.info("所有Sheet写入完成"); } catch (Exception e) {
log.error("Excel导出失败", e);
throw e;
}
} /**
* 写入单个Sheet的数据
*/
private void writeSheetData(ExcelWriter excelWriter, int sheetNo, long start, long end) {
String sheetName = "用户数据" + (sheetNo + 1);
WriteSheet writeSheet = EasyExcel.writerSheet(sheetNo, sheetName).build(); long totalToQuery = end - start;
int totalWritten = 0; // 分批查询并写入,每批查询后立即写入,不缓存大量数据
for (long i = 0; i < totalToQuery; i += QUERY_BATCH_SIZE) {
long currentStart = start + i;
long currentEnd = Math.min(start + i + QUERY_BATCH_SIZE, end); // 调用UserService查询数据
List<User> batchData = userService.findUsersByRange(currentStart, currentEnd); if (batchData == null || batchData.isEmpty()) {
log.info("{} - {} 范围没有数据", currentStart, currentEnd);
break; // 没有更多数据,提前退出
} // 直接写入这一批数据
excelWriter.write(batchData, writeSheet);
totalWritten += batchData.size(); log.info("Sheet {} 已写入 {} - {} 范围的数据,累计 {} 条",
sheetName, currentStart, currentEnd, totalWritten); // 清除引用,帮助GC
batchData = new ArrayList<>();
} log.info("Sheet {} 写入完成,共 {} 条数据", sheetName, totalWritten);
}
}

3️⃣ 效果

指标 优化前 优化后
内存峰值 1.2GB 100MB
耗时 45s 18s

百万级导入实战

1️⃣ 场景

需求 数据量 策略
导入用户表 100万+ 分Sheet + 监听器 + 批量插入

2️⃣ 监听器和Service(核心)

package com.example.easyexcel.listener;

import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.example.easyexcel.model.User;
import com.example.easyexcel.service.UserService;
import lombok.extern.slf4j.Slf4j; import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong; /**
* 用户数据导入监听器(独立类实现)
*/
@Slf4j
public class UserImportListener extends AnalysisEventListener<User> { // 批量保存阈值(可根据内存调整)
private static final int BATCH_SIZE = 5000; // 临时存储批次数据
private final List<User> batchList = new ArrayList<>(BATCH_SIZE); // 导入结果统计
private final AtomicLong successCount = new AtomicLong(0);
private final AtomicLong failCount = new AtomicLong(0); // 业务服务(通过构造器注入)
private final UserService userService; public UserImportListener(UserService userService) {
this.userService = userService;
} /**
* 每读取一行数据触发
*/
@Override
public void invoke(User user, AnalysisContext context) {
// 数据验证
if (validateUser(user)) {
batchList.add(user);
successCount.incrementAndGet(); // 达到批次大小则保存
if (batchList.size() >= BATCH_SIZE) {
saveBatchData();
// 清空列表释放内存
batchList.clear();
}
} else {
failCount.incrementAndGet();
log.warn("数据验证失败: {}", user);
}
} /**
* 所有数据读取完成后触发
*/
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
// 处理剩余数据
if (!batchList.isEmpty()) {
saveBatchData();
batchList.clear();
}
log.info("当前Sheet导入结束,成功: {}, 失败: {}", successCount.get(), failCount.get());
} /**
* 批量保存数据
*/
private void saveBatchData() {
try {
// 调用业务层批量保存(带事务)
userService.batchSaveUsers(batchList);
log.debug("批量保存成功,数量: {}", batchList.size());
} catch (Exception e) {
log.error("批量保存失败,数量: {}", batchList.size(), e);
// 失败处理:可记录失败数据到文件或数据库
handleSaveFailure(batchList);
}
} /**
* 数据验证逻辑
*/
private boolean validateUser(User user) {
// 基础字段验证(根据实际业务调整)
if (user == null) return false;
if (user.getId() == null) return false;
if (user.getName() == null || user.getName().trim().isEmpty()) return false;
return true;
} /**
* 处理保存失败的数据
*/
private void handleSaveFailure(List<User> failedData) {
// 实现失败数据的处理逻辑(例如写入失败日志表)
// userService.saveFailedData(failedData);
} // Getter方法用于统计结果
public long getSuccessCount() {
return successCount.get();
} public long getFailCount() {
return failCount.get();
}
}

导入Service类

package com.example.easyexcel.service;

import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.support.ExcelTypeEnum;
import com.example.easyexcel.listener.SheetCountListener;
import com.example.easyexcel.listener.UserImportListener;
import com.example.easyexcel.model.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile; import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicLong; /**
* 百万级Excel数据导入服务
*/
@Service
@Slf4j
public class ExcelImportService { private final ThreadPoolTaskExecutor excelExecutor;
private final UserService userService; public ExcelImportService(ThreadPoolTaskExecutor excelExecutor, UserService userService) {
this.excelExecutor = excelExecutor;
this.userService = userService; } /**
* 多线程导入百万级用户数据(每个Sheet一个线程)
*/
public void importMillionUsers(MultipartFile file) throws IOException {
// 1. 保存成临时文件,避免多线程共用 InputStream
java.io.File tmpFile = java.io.File.createTempFile("excel_", ".xlsx");
file.transferTo(tmpFile); // Spring 提供的零拷贝
tmpFile.deleteOnExit(); // JVM 退出时自动清理 ExcelTypeEnum excelType = getExcelType(file.getOriginalFilename()); // 2. 拿 sheet 数量
int sheetCount;
try (InputStream in = new java.io.FileInputStream(tmpFile)) {
sheetCount = getSheetCount(in);
}
log.info("开始导入,总 Sheet 数: {}", sheetCount); // 3. 并发读,每个 Sheet 独立 FileInputStream
AtomicLong totalSuccess = new AtomicLong(0);
AtomicLong totalFail = new AtomicLong(0); List<CompletableFuture<Void>> futures = new ArrayList<>(sheetCount);
for (int sheetNo = 0; sheetNo < sheetCount; sheetNo++) {
final int idx = sheetNo;
futures.add(CompletableFuture.runAsync(() -> {
try (InputStream in = new java.io.FileInputStream(tmpFile)) {
UserImportListener listener = new UserImportListener(userService);
EasyExcel.read(in, User.class, listener)
.excelType(excelType)
.sheet(idx)
.doRead(); totalSuccess.addAndGet(listener.getSuccessCount());
totalFail.addAndGet(listener.getFailCount());
log.info("Sheet {} 完成,成功: {}, 失败: {}", idx, listener.getSuccessCount(), listener.getFailCount());
} catch (IOException e) {
throw new RuntimeException("Sheet " + idx + " 读取失败", e);
}
}, excelExecutor));
} CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
log.info("全部导入完成,总成功: {},总失败: {}", totalSuccess.get(), totalFail.get());
} /**
* 获取Excel中的Sheet数量
*/
private int getSheetCount(InputStream inputStream) {
SheetCountListener countListener = new SheetCountListener();
EasyExcel.read(inputStream)
.registerReadListener(countListener)
.doReadAll();
return countListener.getSheetCount();
} /**
* 获取Excel文件类型
*
*/
public ExcelTypeEnum getExcelType(String fileName) {
if (fileName == null) return null;
if (fileName.toLowerCase().endsWith(".xlsx")) {
return ExcelTypeEnum.XLSX;
} else if (fileName.toLowerCase().endsWith(".xls")) {
return ExcelTypeEnum.XLS;
}
return null;
} }

3️⃣ Controller

 @PostMapping("/import")
@ApiOperation("导入用户数据")
public ResponseEntity<String> importUsers(@RequestParam("file") MultipartFile file) {
try {
if (file.isEmpty()) {
return ResponseEntity.badRequest().body("请选择要导入的文件");
} String fileName = file.getOriginalFilename();
ExcelTypeEnum excelType = importService.getExcelType(fileName);
if (excelType == null) {
return ResponseEntity.badRequest().body("不支持的文件类型,文件名:" + fileName);
} importService.importMillionUsers(file);
return ResponseEntity.ok("文件导入成功,正在后台处理数据");
} catch (Exception e) {
log.error("导入用户数据失败", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("导入失败:" + e.getMessage());
}
}

性能优化技巧

技巧 说明
分批查询 避免一次性加载全表
分批写入 每5k条批量插入
临时文件 并发读时先 MultipartFile.transferTo(tmp)
线程池 配置专用线程池,隔离业务线程
# application.yml
spring:
task:
execution:
pool:
core-size: 10
max-size: 30
queue-capacity: 1000

常见问题 & 解决方案

问题 解决方案
Can not create temporary file! 并发读时先保存临时文件,再独立流读取
Stream Closed 每个任务独立 InputStream
OutOfMemoryError 分批处理 + 及时 clear()

总结

Spring Boot + EasyExcel零侵入 的情况下即可完成百万级数据的导入导出。

通过 分批、并发、顺序写 等技巧,内存占用降低 90% 以上。

完整代码参考:springboot-easyexcel-batch


如果本文对你有帮助,欢迎 Star & Fork 源码!

SpringBoot系列之集成EasyExcel实现百万级别的数据导入导出实践的更多相关文章

  1. SpringBoot系列之集成logback实现日志打印(篇二)

    SpringBoot系列之集成logback实现日志打印(篇二) 基于上篇博客SpringBoot系列之集成logback实现日志打印(篇一)之后,再写一篇博客进行补充 logback是一款开源的日志 ...

  2. SpringBoot系列之集成jsp模板引擎

    目录 1.模板引擎简介 2.环境准备 4.源码原理简介 SpringBoot系列之集成jsp模板引擎 @ 1.模板引擎简介 引用百度百科的模板引擎解释: 模板引擎(这里特指用于Web开发的模板引擎)是 ...

  3. SpringBoot系列之集成Druid配置数据源监控

    SpringBoot系列之集成Druid配置数据源监控 继上一篇博客SpringBoot系列之JDBC数据访问之后,本博客再介绍数据库连接池框架Druid的使用 实验环境准备: Maven Intel ...

  4. SpringBoot系列之集成Mybatis教程

    SpringBoot系列之集成Mybatis教程 环境准备:IDEA + maven 本博客通过例子的方式,介绍Springboot集成Mybatis的两种方法,一种是通过注解实现,一种是通过xml的 ...

  5. SpringBoot系列之集成Dubbo的方式

    SpringBoot系列之集成Dubbo的方式 本博客介绍Springboot框架集成Dubbo实现微服务的3种常用方式,对于Dubbo知识不是很熟悉的,请先学习我上一篇博客:SpringBoot系列 ...

  6. 使用表类型(Table Type-SqlServer)实现百万级别的数据一次性毫秒级别插入

    使用表类型(Table Type)实现百万级别的数据一次性插入 思路 1 创建表类型(TaBleType)         2 创建添加存储过程         3 使用C#语言构建一个DataTab ...

  7. Excel如何快速渲染百万级别的数据

    Excel主要经历1.查询2.渲染的方式 关于查询: 不同技术水平的人有不同的解决方案,目前我采用的是 1:多线程查询 2:一个异步后台线程每次查询100条便渲染,采用的“懒加载方式”,这样可以做到实 ...

  8. EasyPoi大数据导入导出百万级实例

    EasyPoi介绍: 利用注解的方式简化了Excel.Word.PDF等格式的导入导出,而且是百万级数据的导入导出.EasyPoi官方网址:EasyPoi教程_V1.0 (mydoc.io).下面我写 ...

  9. SpringBoot系列之集成Thymeleaf用法手册

    目录 1.模板引擎 2.Thymeleaf简介 2.1).Thymeleaf定义 2.2).适用模板 3.重要知识点 3.1).th:text和th:utext 3.2).标准表达式 3.3).Thy ...

  10. SpringBoot系列之集成Dubbo示例教程

    一.分布式基本理论 1.1.分布式基本定义 <分布式系统原理与范型>定义: "分布式系统是若干独立计算机的集合,这些计算机对于用户来说就像单个相关系统" 分布式系统(d ...

随机推荐

  1. codeup之A+B 输入输出练习I 、II 、III、IV、V、VI、VII、VIII(黑盒测试

    不建议做,掌握书上几种情况即可,题简单又重复 I Description 你的任务是计算a+b.这是为了acm初学者专门设计的题目.你肯定发现还有其他题目跟这道题的标题类似,这些问题也都是专门为初学者 ...

  2. 线下IDC数据中心迁移至阿里云详细方案

    一.迁移前准备 1. 迁移规划 资源评估 统计需迁移的数据库类型.版本.数据量(如 MySQL 5.7.SQL Server 2019.文件存储系统等). 评估应用依赖关系,明确停机窗口(建议业务低峰 ...

  3. SQL解析工具JSQLParser

    一.引言 JSQLParser(GitHub:https://github.com/JSQLParser/JSqlParser)是一个Java语言的SQL语句解析工具,功能十分强大,它可以将SQL语句 ...

  4. Java中的静态块(static{})

    静态块(static{}) (1) static关键字还有一个比较关键的作用,用来形成静态代码块(static{} 即static块 )以优化程序性能. (2) static块可以置于类中的任何地方, ...

  5. mysql安全小结

    sql的注入是一个很困扰人的问题,一些恶意攻击者可以利用sql注入来获取甚至是修改数据库中的信息,尤其是一些比较敏感的密码一类的数据. sql注入主要利用mysql 的注释将后续应正常执行的语句注释掉 ...

  6. 【中英】【吴恩达课后测验】Course 3 -结构化机器学习项目 - 第二周测验

    [中英][吴恩达课后测验]Course 3 -结构化机器学习项目 - 第二周测验 - 自动驾驶(案例研究) 上一篇:[课程3 - 第一周测验]※※※※※ [回到目录]※※※※※下一篇:[课程4 -第一 ...

  7. 爬虫1——urllib的使用

    一.什么是爬虫 1.爬虫Spider的概念 爬虫用于爬取数据,又称之为数据采集程序. 爬取的数据来源于网络,网络中的数据可以是由WEB服务器(Nginx/Apache),数据库服务器(MySQL.Re ...

  8. 题解:P6880 [JOI 2020 Final] オリンピックバス

    一个比较重要的性质:反转的边要在最短路上才会有贡献. 我们可以先跑一遍最短路,记录下整颗最短路树,然后暴力的对每一条边进行判断,反转. 我们建正反图各两个,分别以 \(1\),\(n\) 为起点.\( ...

  9. 壹伴助手_秀米编辑器官网_微信公众号图文编辑和H5制作- 秀米XIUMI

    壹伴助手:公众号编辑器的首选工具 壹伴是一款比秀米更加优秀的公众号编辑器工具,作为第一名推荐,是所有公众号小编的首要选择. 在新时代的自媒体运营中,排版不仅是一项技能,更是一种艺术.无论是文字的优美书 ...

  10. ChunJun框架在数据还原上的探索和实践 | Hadoop Meetup精彩回顾

    Hadoop是Apache基金会旗下最知名的基础架构开源项目之一.自2006年诞生以来,逐步发展成为海量数据存储.处理最为重要的基础组件,形成了非常丰富的技术生态. 作为国内顶尖的 Hadoop 开源 ...