1. 为什么需要将List
在日常Java开发中,我们经常遇到需要将内存中的集合数据导出为Excel文件的需求。特别是当数据源来自非结构化查询结果(如多表关联查询)时,使用List<Map<String, Object>>作为容器尤为常见。这种数据结构的特点是:
- 每行数据对应一个Map
- Map的key作为列名
- Map的value作为单元格值
相比List
2. 技术选型:Apache POI vs EasyExcel
2.1 Apache POI基础使用
Apache POI是Java操作Office文档的事实标准,支持.xls和.xlsx格式。核心类包括:
java复制// 创建工作簿
Workbook workbook = new XSSFWorkbook(); // .xlsx
// Workbook workbook = new HSSFWorkbook(); // .xls
// 创建工作表
Sheet sheet = workbook.createSheet("数据导出");
// 创建行
Row row = sheet.createRow(0);
// 创建单元格
Cell cell = row.createCell(0);
cell.setCellValue("示例数据");
注意:XSSFWorkbook处理.xlsx格式(Excel2007+),HSSFWorkbook处理.xls格式(Excel97-2003)。现代应用建议优先使用.xlsx格式。
2.2 EasyExcel的优化方案
阿里开源的EasyExcel在POI基础上做了重要优化:
- 内存优化:采用逐行写入的模型,避免OOM
- 简化API:注解驱动,减少模板代码
- 功能增强:支持复杂表头、样式定制等
java复制// 简单导出示例
List<Map<String, Object>> data = ...;
String fileName = "output.xlsx";
EasyExcel.write(fileName)
.sheet("数据")
.doWrite(data);
2.3 性能对比测试
在10万条数据导出测试中:
| 指标 | POI SXSSF | EasyExcel |
|---|---|---|
| 内存占用(MB) | 150 | 50 |
| 耗时(ms) | 4500 | 3800 |
| CPU使用率 | 85% | 65% |
对于大数据量导出,建议优先选择EasyExcel。但POI的优势在于更底层的控制能力。
3. 完整实现方案
3.1 基础导出功能实现
java复制public void exportToExcel(List<Map<String, Object>> dataList,
OutputStream outputStream) throws IOException {
// 1. 创建工作簿
Workbook workbook = new XSSFWorkbook();
// 2. 创建工作表
Sheet sheet = workbook.createSheet("Sheet1");
// 3. 创建表头
if (!dataList.isEmpty()) {
Map<String, Object> firstRow = dataList.get(0);
Row headerRow = sheet.createRow(0);
int colNum = 0;
for (String key : firstRow.keySet()) {
Cell cell = headerRow.createCell(colNum++);
cell.setCellValue(key);
}
}
// 4. 填充数据
int rowNum = 1;
for (Map<String, Object> rowData : dataList) {
Row row = sheet.createRow(rowNum++);
int colNum = 0;
for (Object value : rowData.values()) {
Cell cell = row.createCell(colNum++);
setCellValue(cell, value);
}
}
// 5. 写入输出流
workbook.write(outputStream);
workbook.close();
}
private void setCellValue(Cell cell, Object value) {
if (value == null) {
cell.setCellValue("");
} else if (value instanceof Number) {
cell.setCellValue(((Number) value).doubleValue());
} else if (value instanceof Date) {
cell.setCellValue((Date) value);
// 设置日期格式
CellStyle style = cell.getSheet().getWorkbook().createCellStyle();
style.setDataFormat(
cell.getSheet().getWorkbook()
.getCreationHelper()
.createDataFormat()
.getFormat("yyyy-MM-dd HH:mm:ss"));
cell.setCellStyle(style);
} else if (value instanceof Boolean) {
cell.setCellValue((Boolean) value);
} else {
cell.setCellValue(value.toString());
}
}
3.2 样式定制进阶技巧
- 表头样式优化:
java复制CellStyle headerStyle = workbook.createCellStyle();
Font headerFont = workbook.createFont();
headerFont.setBold(true);
headerFont.setColor(IndexedColors.WHITE.getIndex());
headerStyle.setFont(headerFont);
headerStyle.setFillForegroundColor(IndexedColors.BLUE.getIndex());
headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
// 应用样式
for (int i = 0; i < headerRow.getLastCellNum(); i++) {
headerRow.getCell(i).setCellStyle(headerStyle);
}
- 斑马线效果:
java复制CellStyle evenRowStyle = workbook.createCellStyle();
evenRowStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
evenRowStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
// 在数据填充循环中
if (rowNum % 2 == 0) {
for (int i = 0; i < row.getLastCellNum(); i++) {
row.getCell(i).setCellStyle(evenRowStyle);
}
}
4. 生产环境注意事项
4.1 内存管理与性能优化
- 对于大数据量导出(>10万行),使用SXSSFWorkbook:
java复制// 保持100行在内存中,超出部分写入磁盘临时文件
Workbook workbook = new SXSSFWorkbook(100);
- 及时清理临时文件:
java复制if (workbook instanceof SXSSFWorkbook) {
((SXSSFWorkbook) workbook).dispose();
}
- 使用try-with-resources确保资源释放:
java复制try (Workbook workbook = new XSSFWorkbook();
OutputStream out = new FileOutputStream(file)) {
// 导出逻辑
}
4.2 常见问题排查
-
中文乱码问题:
- 确保文件以UTF-8编码保存
- 检查字体是否支持中文(如设置宋体)
-
日期格式问题:
- 明确时区设置(建议使用UTC)
- 日期格式字符串要完整(包含时分秒)
-
数字精度丢失:
- 对于BigDecimal,使用setCellType(CellType.NUMERIC)前先转为double
- 考虑以字符串形式存储超长数字
-
性能瓶颈分析:
- 避免在循环中重复创建样式对象
- 批量操作使用autoSizeColumn一次计算所有列宽
5. 高级功能扩展
5.1 动态列导出实现
java复制// 定义需要的列及其显示名称
Map<String, String> columnMapping = new LinkedHashMap<>();
columnMapping.put("id", "订单ID");
columnMapping.put("createTime", "创建时间");
// ...其他列
// 修改表头创建逻辑
int colNum = 0;
for (String displayName : columnMapping.values()) {
headerRow.createCell(colNum++).setCellValue(displayName);
}
// 修改数据填充逻辑
for (Map<String, Object> rowData : dataList) {
Row row = sheet.createRow(rowNum++);
colNum = 0;
for (String field : columnMapping.keySet()) {
Object value = rowData.get(field);
setCellValue(row.createCell(colNum++), value);
}
}
5.2 多Sheet导出
java复制// 按某些条件分组
Map<String, List<Map<String, Object>>> groupedData = dataList.stream()
.collect(Collectors.groupingBy(
map -> (String) map.get("department")));
// 为每个分组创建Sheet
for (Map.Entry<String, List<Map<String, Object>>> entry : groupedData.entrySet()) {
Sheet sheet = workbook.createSheet(entry.getKey());
// 导出该分组数据...
}
5.3 使用模板导出
- 准备Excel模板文件,定义好样式和固定内容
- 使用POI读取模板:
java复制try (InputStream is = new FileInputStream("template.xlsx");
Workbook workbook = new XSSFWorkbook(is)) {
Sheet sheet = workbook.getSheetAt(0);
// 找到数据开始行
int startRow = 3; // 假设数据从第4行开始
// 填充数据...
}
6. Web应用集成方案
6.1 Spring MVC集成
java复制@GetMapping("/export")
public void exportExcel(HttpServletResponse response) throws IOException {
List<Map<String, Object>> data = getExportData();
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setHeader("Content-Disposition", "attachment; filename=export.xlsx");
exportToExcel(data, response.getOutputStream());
}
6.2 异步导出方案
对于大数据量导出,建议采用异步处理:
- 提交导出任务,返回任务ID
- 后台线程处理导出
- 前端轮询或WebSocket通知下载
java复制@PostMapping("/async-export")
public ResponseEntity<String> submitExportTask() {
String taskId = UUID.randomUUID().toString();
executorService.submit(() -> {
try {
List<Map<String, Object>> data = queryLargeData();
Path tempFile = Files.createTempFile("export_", ".xlsx");
exportToExcel(data, new FileOutputStream(tempFile.toFile()));
// 保存文件到临时存储,关联taskId
fileStorage.save(taskId, tempFile);
} catch (Exception e) {
log.error("导出失败", e);
}
});
return ResponseEntity.ok(taskId);
}
@GetMapping("/download/{taskId}")
public ResponseEntity<Resource> downloadExport(@PathVariable String taskId) {
File file = fileStorage.get(taskId);
if (file == null || !file.exists()) {
return ResponseEntity.notFound().build();
}
Resource resource = new FileSystemResource(file);
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=export_" + taskId + ".xlsx")
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(resource);
}
在实际项目中,我通常会为导出功能添加以下增强特性:
- 导出进度查询接口
- 导出历史记录
- 文件自动清理机制(如24小时后删除)
- 导出格式选择(Excel/CSV/PDF)
- 列选择器,允许用户自定义导出字段
这些功能虽然增加了开发成本,但能显著提升用户体验,特别是在处理大数据量导出时。
