项目结构概览
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>
百万级导出实战
场景
需求 | 数据量 | 策略 |
---|---|---|
导出用户表 | 100万+ | 分sheet + 分批查询 + 边查边写 |
核心代码
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); } }
效果
指标 | 优化前 | 优化后 |
---|---|---|
内存峰值 | 1.2gb | 100mb |
耗时 | 45s | 18s |
百万级导入实战
场景
需求 | 数据量 | 策略 |
---|---|---|
导入用户表 | 100万+ | 分sheet + 监听器 + 批量插入 |
监听器和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; } }
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实现百万级别的数据导入导出实践指南的详细内容,更多关于springboot easyexcel数据导入导出的资料请关注代码网其它相关文章!
发表评论