Spring Boot+EasyExcel百万级数据导出优化方案

发布时间:2026/7/3 5:53:11

Spring Boot+EasyExcel百万级数据导出优化方案 1. 项目概述在Java后端开发中Excel导出是一个常见但容易出问题的功能点。当数据量达到百万级别时传统的POI或EasyExcel全量导出方式极易引发OOM内存溢出问题。本文将分享一个基于Spring Boot和EasyExcel的百万级数据导出解决方案通过分页查询、分批写入和异步处理等核心技术手段彻底解决大数据量导出的内存问题。这个方案的核心价值在于小数据量1万条直接全量导出简单高效大数据量1万-50万条采用分页查询分批写入避免内存溢出超大数据量百万级引入异步导出机制支持进度查询和结果下载2. 技术选型与原理2.1 为什么选择EasyExcelEasyExcel是阿里巴巴开源的一款Excel处理工具相比传统Apache POI有以下优势内存优化采用逐行解析模式不会一次性加载整个文件到内存API简洁链式调用风格代码可读性高功能丰富支持复杂表头、合并单元格、自定义样式等性能优异实测百万数据导出时间在3-5分钟取决于硬件配置2.2 内存问题根源分析传统导出方案的OOM问题主要来自两个环节数据加载阶段一次性从数据库查询全量数据到内存Excel构建阶段在内存中构建完整的Excel对象模型我们的解决方案通过以下方式规避这些问题分页查询每次只加载部分数据如3000条分批写入每批数据写入后立即释放内存流式输出通过OutputStream直接写入响应不缓存完整文件3. 核心实现详解3.1 基础环境准备首先确保项目中已引入必要依赖!-- EasyExcel核心依赖 -- dependency groupIdcom.alibaba/groupId artifactIdeasyexcel/artifactId version3.3.2/version /dependency !-- Spring Web相关 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency3.2 核心工具类设计3.2.1 ExcelExporter工具类作为统一入口封装了两种导出模式public class ExcelExporter { // 分页导出大数据量 public static T void exportByPage(HttpServletResponse response, String fileName, String sheetName, ClassT dataModel, int pageSize, int totalCount, PageQuerySupplierT pageSupplier) { // 设置响应头 setupResponse(response, fileName); try (OutputStream out response.getOutputStream()) { PageWriteExcelHelper.writeByPage(out, dataModel, sheetName, pageSize, totalCount, pageSupplier); } catch (Exception e) { throw new RuntimeException(导出失败, e); } } // 简单导出小数据量 public static T void exportSimple(HttpServletResponse response, String fileName, String sheetName, ClassT dataModel, ListT dataList) { setupResponse(response, fileName); try (OutputStream out response.getOutputStream()) { EasyExcel.write(out, dataModel) .sheet(sheetName) .doWrite(dataList); } catch (Exception e) { throw new RuntimeException(导出失败, e); } } // 响应头设置私有方法 private static void setupResponse(HttpServletResponse response, String fileName) { // 设置Content-Type和编码 response.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); response.setCharacterEncoding(UTF-8); // 处理文件名中的中文和空格 String encodedFileName URLEncoder.encode(fileName, UTF-8) .replaceAll(\\, %20); response.setHeader(Content-disposition, attachment;filename encodedFileName .xlsx); } // 分页查询函数式接口 FunctionalInterface public interface PageQuerySupplierT { ListT getPage(int pageNum, int pageSize); } }3.2.2 PageWriteExcelHelper分页写入核心public class PageWriteExcelHelper { public static T void writeByPage(OutputStream outputStream, ClassT head, String sheetName, int pageSize, int totalCount, PageQuerySupplierT supplier) { ExcelWriter excelWriter EasyExcel.write(outputStream, head).build(); WriteSheet writeSheet EasyExcel.writerSheet(sheetName).build(); try { // 计算总页数 int totalPage totalCount 0 ? (int) Math.ceil((double) totalCount / pageSize) : 1; // 分页循环处理 for (int pageNum 1; pageNum totalPage; pageNum) { ListT pageData supplier.getPage(pageNum, pageSize); excelWriter.write(pageData, writeSheet); pageData.clear(); // 关键立即释放当前页内存 } } finally { if (excelWriter ! null) { excelWriter.finish(); // 必须关闭资源 } } } }3.3 业务层实现3.3.1 数据模型定义Data public class User { ExcelProperty(用户ID) private Long id; ExcelProperty(用户名) private String username; ExcelProperty(手机号) private String phone; ExcelProperty(创建时间) private String createTime; }3.3.2 Service层实现Service public class UserService { Autowired private UserMapper userMapper; // 全量查询仅用于小数据量 public ListUser findAllUsers() { return userMapper.selectAll(); } // 分页查询大数据量核心 public ListUser findByPage(int pageNum, int pageSize) { int offset (pageNum - 1) * pageSize; return userMapper.selectByPage(offset, pageSize); } // 查询总数 public int countTotalUsers() { return userMapper.countTotal(); } }3.3.3 Controller层接口RestController RequestMapping(/export) public class ExportController { Autowired private UserService userService; // 小数据量导出 GetMapping(/small) public void exportSmall(HttpServletResponse response) { ListUser data userService.findAllUsers(); ExcelExporter.exportSimple(response, 用户列表, 用户数据, User.class, data); } // 大数据量导出 GetMapping(/large) public void exportLarge(HttpServletResponse response) { int total userService.countTotalUsers(); ExcelExporter.exportByPage(response, 全量用户, 用户清单, User.class, 3000, total, (pageNum, size) - userService.findByPage(pageNum, size)); } }4. 高级功能异步出对于百万级数据同步导出会导致请求超时必须采用异步方案。4.1 线程池配置Configuration public class ThreadPoolConfig { Bean public ThreadPoolExecutor exportExecutor() { return new ThreadPoolExecutor( 5, 10, 60, TimeUnit.SECONDS, new LinkedBlockingQueue(100), new ThreadFactory() { private int count 0; Override public Thread newThread(Runnable r) { return new Thread(r, export- (count)); } }, new ThreadPoolExecutor.CallerRunsPolicy() ); } }4.2 异步导出实现RestController RequestMapping(/async-export) public class AsyncExportController { Autowired private UserService userService; Autowired private ThreadPoolExecutor exportExecutor; Autowired private ExportTaskService taskService; GetMapping(/trigger) public String triggerExport() { String taskId TASK_ System.currentTimeMillis(); exportExecutor.execute(() - { // 初始化任务状态 taskService.startTask(taskId); try { int total userService.countTotalUsers(); String filePath /tmp/export/ taskId .xlsx; // 分页导出到临时文件 try (FileOutputStream out new FileOutputStream(filePath)) { PageWriteExcelHelper.writeByPage(out, User.class, 用户数据, 3000, total, (pageNum, size) - { ListUser page userService.findByPage(pageNum, size); // 更新进度 taskService.updateProgress(taskId, pageNum * size * 100 / total); return page; }); } // 标记任务完成 taskService.completeTask(taskId, filePath); } catch (Exception e) { taskService.failTask(taskId, e.getMessage()); } }); return 导出任务已启动ID: taskId; } GetMapping(/progress/{taskId}) public ExportProgress getProgress(PathVariable String taskId) { return taskService.getProgress(taskId); } GetMapping(/download/{taskId}) public void download(PathVariable String taskId, HttpServletResponse response) { ExportTask task taskService.getTask(taskId); if (task null || !COMPLETED.equals(task.getStatus())) { throw new RuntimeException(任务不存在或未完成); } File file new File(task.getFilePath()); try (InputStream in new FileInputStream(file); OutputStream out response.getOutputStream()) { response.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); response.setHeader(Content-disposition, attachment;filenameexport_ taskId .xlsx); byte[] buffer new byte[1024]; int len; while ((len in.read(buffer)) 0) { out.write(buffer, 0, len); } } catch (Exception e) { throw new RuntimeException(下载失败, e); } } }5. 性能优化与注意事项5.1 关键参数调优页大小选择建议范围1000-5000条/页内存充足可适当增大减少IO次数内存紧张减小页大小降低单次内存占用线程池配置核心线程数CPU核心数1最大线程数根据系统负载调整队列容量避免设置过大导致内存积压5.2 常见问题排查问题现象可能原因解决方案导出文件损坏ExcelWriter未关闭确保finally块调用finish()内存溢出页大小设置过大减小pageSize参数导出速度慢数据库查询慢优化SQL添加索引文件名乱码编码问题使用URLEncoder处理文件名5.3 最佳实践建议生产环境建议异步导出文件存储到OSS等对象存储定期清理临时文件建议使用Spring的Scheduled添加导出权限控制和操作日志监控指标导出任务平均耗时内存使用峰值并发导出任务数扩展思考支持CSV格式导出数据量更大时添加导出模板自定义功能实现断点续传功能超大数据量6. 实测数据与效果对比我们在测试环境4核8G进行了性能测试数据量传统方式分页方式内存占用对比1万条1.2s1.5s300MB vs 50MB10万条OOM8s- vs 80MB100万条OOM85s- vs 100MB关键发现小数据量时性能差异不大10万条以上传统方式必然OOM分页方式内存占用稳定与数据量无关7. 完整代码获取与使用本文涉及的完整代码已托管至GitHub仓库包含核心工具类ExcelExporter、PageWriteExcelHelperSpring Boot集成示例异步导出完整实现单元测试用例使用步骤克隆仓库导入IDE修改application.yml中的数据库配置运行ExportApplication启动类访问/swagger-ui.html查看接口文档实际项目中建议将核心工具类打包为独立模块通过Maven依赖引入。

相关新闻