1. 项目概述与需求背景
在日常业务开发中,我们经常遇到这样的场景:需要将Excel表格中的每行数据转换为独立的Excel文件,并打包成ZIP压缩包供用户下载。这种需求在数据报表导出、批量文件生成等业务场景中非常常见。比如在林业调查系统中,每行数据代表一个地块的详细信息,需要为每个地块生成独立的调查表文件。
传统做法是使用Apache POI库来处理Excel文件,但POI的API较为复杂,内存消耗大,处理大数据量时容易OOM。而阿里的EasyExcel则提供了更简洁的API和更好的内存管理,特别适合这种批量导出场景。
2. 技术选型与核心组件
2.1 EasyExcel的优势
EasyExcel相比传统POI有几个显著优势:
- 内存优化:采用逐行读写模式,避免一次性加载整个文件到内存
- API简洁:通过注解方式定义模型,简化了读写操作
- 模板填充:支持基于模板的导出,保持样式和格式
- 性能优异:实测百万级数据导出内存占用稳定
2.2 项目依赖
核心依赖如下(Maven配置):
xml复制<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.3.2</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
3. 核心实现详解
3.1 数据模型定义
首先定义Excel行数据对应的实体类,使用EasyExcel的注解标记字段映射:
java复制@Data
public class Demo2 {
@ExcelProperty("代码")
private String zddm;
@ExcelProperty("不动产单元代码")
private String bdcdydm;
@ExcelProperty("名称")
private String mc;
// 其他字段...
}
注意事项:
@ExcelProperty注解的值必须与Excel表头完全一致- 建议使用
@Data注解自动生成getter/setter- 字段顺序决定了Excel中的列顺序
3.2 模板文件准备
准备一个包含所有必要样式和表头的Excel模板文件,存放在resources/static/template2.xlsx。模板中应包含占位符(如${zddm})对应实体类字段。
实操技巧:
- 使用Excel的"冻结窗格"功能固定表头
- 预先设置好单元格格式(如日期、数字格式)
- 模板文件不宜过大,避免影响性能
3.3 核心处理流程
3.3.1 文件上传与读取
java复制@PostMapping("/upload2")
public ResponseEntity<Resource> upload(@ModelAttribute MultipartFile file) {
// 验证文件非空
if (file == null || file.isEmpty()) {
throw new IllegalArgumentException("上传文件不能为空");
}
// 读取Excel数据
List<Demo2> rows = EasyExcel.read(file.getInputStream())
.head(Demo2.class)
.sheet()
.headRowNumber(1) // 从第2行开始读(跳过表头)
.autoTrim(true) // 自动去除空格
.doReadSync();
}
3.3.2 单文件生成逻辑
java复制private void processSingleDemo(Demo2 demo, int index,
ClassPathResource templateResource,
ZipOutputStream zos) throws IOException {
// 创建临时Excel文件
File tempExcelFile = File.createTempFile("temp_excel", ".xlsx");
try (InputStream templateStream = templateResource.getInputStream();
FileOutputStream excelFos = new FileOutputStream(tempExcelFile)) {
// 使用模板填充数据
ExcelWriter excelWriter = EasyExcel.write(excelFos)
.withTemplate(templateStream)
.build();
WriteSheet writeSheet = EasyExcel.writerSheet().build();
excelWriter.fill(demo, writeSheet);
excelWriter.finish();
// 添加到ZIP
String fileName = generateFileName(demo, index);
zos.putNextEntry(new ZipEntry(fileName));
Files.copy(tempExcelFile.toPath(), zos);
zos.closeEntry();
} finally {
// 删除临时文件
if (tempExcelFile.exists()) {
tempExcelFile.delete();
}
}
}
3.3.3 文件名生成策略
java复制private String generateFileName(Demo2 demo, int index) {
StringBuilder fileName = new StringBuilder("调查表");
if (StringUtils.isNotBlank(demo.getZddm())) {
fileName.append("_").append(demo.getZddm().trim());
} else {
fileName.append("_").append(index + 1);
}
return fileName.append(".xlsx").toString();
}
3.4 文件下载与编码处理
java复制private String encodeFileName(String fileName, HttpServletRequest request) {
String userAgent = request.getHeader("User-Agent");
try {
if (userAgent.contains("MSIE") || userAgent.contains("Trident")) {
// IE浏览器特殊处理
return URLEncoder.encode(fileName, "UTF-8");
} else if (userAgent.contains("Firefox")) {
// Firefox特殊处理
return new String(fileName.getBytes(StandardCharsets.UTF_8),
StandardCharsets.ISO_8859_1);
} else {
// 现代浏览器
return URLEncoder.encode(fileName, "UTF-8").replace("+", "%20");
}
} catch (UnsupportedEncodingException e) {
return "download.zip"; // 降级处理
}
}
4. 性能优化与注意事项
4.1 内存管理
- 使用临时文件:每个生成的Excel先写入临时文件,再添加到ZIP,避免内存堆积
- 及时清理资源:所有流资源使用try-with-resources确保关闭
- 批量处理限制:建议单次处理不超过1万条数据,大数据量应分批次处理
4.2 异常处理要点
- 模板文件检查:启动时验证模板文件是否存在
- 空数据校验:拒绝处理空文件或空行
- 临时文件清理:finally块中确保删除临时文件
- 错误信息友好:捕获具体异常,返回用户可理解的错误信息
4.3 常见问题排查
-
中文乱码问题:
- 确保模板文件使用UTF-8编码
- 文件名编码使用RFC 5987标准
- 响应头设置
filename*=UTF-8''
-
样式丢失问题:
- 检查模板中的样式是否正确定义
- 避免在代码中动态修改样式(优先在模板中预设)
-
性能瓶颈:
- 大量数据时考虑分片处理
- 使用
SXSSFWorkbook模式(EasyExcel自动支持)
5. 扩展与进阶
5.1 动态模板支持
可以通过参数化指定模板路径,实现多模板支持:
java复制@Value("${excel.template.path}")
private String templatePath;
public void setTemplate(String templateName) {
this.teplateFileName = "static/" + templateName + ".xlsx";
}
5.2 多Sheet导出
对于复杂数据,可以在一个Excel中生成多个Sheet:
java复制ExcelWriter writer = EasyExcel.write(outputStream).build();
WriteSheet sheet1 = EasyExcel.writerSheet(0, "Sheet1").build();
WriteSheet sheet2 = EasyExcel.writerSheet(1, "Sheet2").build();
writer.fill(data1, sheet1);
writer.fill(data2, sheet2);
writer.finish();
5.3 云端存储集成
结合OSS等云存储服务,可以实现:
- 模板文件从OSS读取
- 生成的ZIP直接上传到OSS
- 返回下载链接而非文件流
java复制// 阿里云OSS示例
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
ossClient.putObject(bucketName, objectName, new FileInputStream(tempZipFile));
String url = ossClient.generatePresignedUrl(bucketName, objectName,
new Date(System.currentTimeMillis() + 3600 * 1000)).toString();
在实际项目中,我遇到过一个典型问题:当处理超过5000条数据时,服务器内存急剧上升。通过分析发现是临时文件没有及时清理导致的。解决方案是在每个文件处理完成后立即执行System.gc()提示JVM回收资源,同时将临时文件统一存放在特定目录,在服务启动时清空该目录。这个经验告诉我,在处理批量文件时,资源管理比功能实现更需要重视。