使用 EasyExcel 导出 Excel 的性能分析与实践
本文旨在探讨使用 EasyExcel 导出大量数据到 Excel 时可能遇到的性能问题,并通过代码示例和实验数据分析不同导出策略(单 Sheet 页 vs. 多 Sheet 页)对导出和打开速度的影响。
核心代码示例
以下 Java 代码展示了如何使用 EasyExcel 将大量数据导出到 Excel 文件。代码中包含了两种导出方法:一种是创建多个 Sheet 页,另一种是使用单个 Sheet 页。
导出多 Sheet 页 (downExcel 方法)
package org.example.easyexcel.controller;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import lombok.extern.slf4j.Slf4j;
import org.example.easyexcel.entity.ExportDataModel; // 假设这是你的数据实体类
import org.example.easyexcel.service.IExportDataService; // 假设这是你的服务接口
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLEncoder;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@Slf4j
@ResponseBody
@Controller
public class ExcelExportController {
@Autowired
IExportDataService exportDataService; // 注入服务层
@GetMapping("download-multi-sheet/{recordCount}")
public void downloadMultiSheet(HttpServletResponse response, @PathVariable("recordCount") Integer recordCount) throws IOException {
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
String fileName = URLEncoder.encode("多Sheet导出示例", "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
List<ExportDataModel> dataList = generateMockData(recordCount);
long startTime = System.currentTimeMillis();
// 使用 try-with-resources 确保 ExcelWriter 被正确关闭
try (ExcelWriter writer = EasyExcel.write(response.getOutputStream(), ExportDataModel.class).build()) {
// 示例:导出 5 个 Sheet 页
for (int i = 0; i < 5; i++) {
// 必须为每个 Sheet 指定唯一的 sheetNo 和 sheetName
WriteSheet writeSheet = EasyExcel.writerSheet(i, "数据模板_" + i).build();
// 实际应用中,这里的 dataList 应该根据分页从数据库查询
writer.write(dataList, writeSheet);
}
}
log.info("多 Sheet 页导出耗时: {} ms", System.currentTimeMillis() - startTime);
}
/**
* 文件下载 (单 Sheet 页)
* @param response HttpServletResponse
* @param recordCount 要导出的记录数量
*/
@GetMapping("/download-single-sheet/{recordCount}")
public void downloadSingleSheet(HttpServletResponse response, @PathVariable("recordCount") Integer recordCount) throws IOException {
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
String fileName = URLEncoder.encode("单Sheet导出示例", "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
List<ExportDataModel> itemData = generateMockData(recordCount);
long startTime = System.currentTimeMillis();
// 直接使用 doWrite 方法导出到单个 Sheet
EasyExcel.write(response.getOutputStream(), ExportDataModel.class)
.sheet("汇总数据")
.doWrite(itemData);
log.info("单 Sheet 页导出耗时: {} ms", System.currentTimeMillis() - startTime);
}
/**
* 模拟从数据库查询数据的逻辑
* @param start 起始索引
* @param end 结束索引
* @return 数据列表
*/
private Collection<ExportDataModel> queryDataFromDb(int start, int end) {
QueryWrapper<ExportDataModel> query = new QueryWrapper<>();
query.last("limit " + start + "," + end); // 假设数据库支持 limit
List<ExportDataModel> list = exportDataService.list(query);
log.info("从数据库查询到 {} 条记录", list.size());
return list;
}
/**
* 生成模拟数据
* @param count 数据量
* @return 模拟数据列表
*/
private List<ExportDataModel> generateMockData(int count) {
LocalDateTime now = LocalDateTime.now();
ExportDataModel mockRecord = new ExportDataModel()
.setAsyncId("MOCK_ID_123")
.setAsyncType("MOCK_TYPE")
.setRoutingKey("mock.routing.key")
.setExchange("mock.exchange")
.setParamClassName("org.example.easyexcel.entity.ExportDataModel")
.setParamJsonDetail("{}")
.setExecStatus("INIT")
.setUpdateTime(now)
.setCreateTime(now)
.setExecCount(1)
.setSendCount(0)
.setExtInfo("{}");
List<ExportDataModel> generatedList = new ArrayList<>(count);
for (int i = 0; i < count; i++) {
// 为确保数据不完全重复,可以稍微修改
mockRecord.setAsyncId("MOCK_ID_" + i);
generatedList.add(mockRecord);
}
log.info("生成 {} 条模拟数据", generatedList.size());
return generatedList;
}
/*
* // 文件上传示例 (注释掉,仅供参考)
* @PostMapping("upload")
* @ResponseBody
* public String uploadFile(MultipartFile file) throws IOException {
* EasyExcel.read(file.getInputStream(), ExportDataModel.class, new UploadDataListener(uploadDAO)).sheet().doRead();
* return "Upload successful";
* }
*/
}
性能测试与分析
在排除数据库 I/O 耗时后,我们对导出百万级数据的性能进行了测试:
- 导出耗时:
- 100,000 条数据:约 2.x 秒
- 500,000 条数据:约 12-13 秒
- 1,000,000 条数据:约 26-28 秒
- 多 Sheet 页 (10 个 Sheet) vs. 单 Sheet 页: 在导出大量数据(如一百万条)时,多 Sheet 页和单 Sheet 页的导出时间几乎没有显著差异。
- 打开 Excel 文件:
- 100,000 条数据 (单 Sheet):打开速度非常快。
- 1,000,000 条数据 (单 Sheet):打开需要约 14 秒。
- 多 Sheet 页 (10 页) 和单 Sheet 页在打开速度上表现相似,差异不大。
结论与建议
EasyExcel 在导出大量数据时表现出良好的性能。在导出时间方面,单 Sheet 页和多 Sheet 页策略的差异可以忽略不计。然而,在打开超大 Excel 文件时,文件大小本身会成为瓶颈,因此,对于超大规模数据,应考虑其他数据展示方案或优化 Excel 文件结构。
重要提示: EasyExcel 本身在写入数据时已内置多线程优化。然而,根据官方文档和社区反馈,EasyExcel 不允许在多线程环境下同时操作多个 Sheet 页。如果在导出过程中手动启用了多线程处理多个 Sheet,可能会导致不可预测的问题。这可能是多 Sheet 页导出性能未显著优于单 Sheet 页的原因之一。
因此,在实际应用中,如果业务逻辑不需要强制划分多个 Sheet,使用单 Sheet 页导出通常更简单且性能表现稳定。如果必须使用多 Sheet,请确保按顺序写入,避免并发写入的风险。