上一篇
Java可通过Apache POI库生成Excel,或使用iText/PDFBox生成PDF,需添加对应依赖,创建工作簿/文档对象,填充数据
在Java开发中,数据导出是常见需求,通常涉及将数据库查询结果、集合对象或业务数据转换为可下载的文件格式(如Excel、CSV、PDF等),以下是完整的实现方案及技术要点解析:
核心思路与技术选型
| 目标格式 | 推荐库 | 适用场景 | 优势 |
|---|---|---|---|
| Excel | Apache POI / EasyExcel | 复杂表格、公式、图表 | 功能强大,支持.xls/.xlsx |
| CSV | OpenCSV / Jackson/Gson | 轻量化文本交换 | 跨平台兼容,解析速度快 |
| iText / PDFBox | 固定排版文档 | 可视化效果好,打印友好 | |
| HTML | Thymeleaf/Freemarker | 邮件模板、网页预览 | 灵活渲染,结合前端样式 |
️ 关键注意事项:
- 内存管理:百万级数据需采用SAX模式(事件驱动)而非DOM全量加载
- 编码规范:中文字符必须使用UTF-8编码,避免乱码
- 安全性:禁止直接写入绝对路径,建议通过
ServletOutputStream传输 - 性能优化:大数据量建议分批次处理(每批5000条),异步任务队列更佳
主流导出方案详解
方案1:Excel导出(Apache POI + Spring Boot)
<!-Maven依赖 -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.2.3</version>
</dependency>
核心代码实现:
public void exportToExcel(HttpServletResponse response, List<User> dataList) throws IOException {
// 1. 创建工作簿 & 工作表
Workbook workbook = new XSSFWorkbook(); // .xlsx格式
Sheet sheet = workbook.createSheet("用户列表");
// 2. 创建表头样式
CellStyle headerStyle = workbook.createCellStyle();
headerStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
headerStyle.setAlignment(HorizontalAlignment.CENTER);
// 3. 写入表头
Row headerRow = sheet.createRow(0);
String[] titles = {"ID", "姓名", "年龄", "注册时间"};
for (int i=0; i<titles.length; i++) {
Cell cell = headerRow.createCell(i);
cell.setCellValue(titles[i]);
cell.setCellStyle(headerStyle);
}
// 4. 填充数据行
CreationHelper createHelper = workbook.getCreationHelper();
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
for (int rowNum=1; rowNum<=dataList.size(); rowNum++) {
User user = dataList.get(rowNum-1);
Row row = sheet.createRow(rowNum);
row.createCell(0).setCellValue(user.getId());
row.createCell(1).setCellValue(user.getName());
row.createCell(2).setCellValue(user.getAge());
row.createCell(3).setCellValue(df.format(user.getRegisterTime()));
}
// 5. 自动调整列宽
for (int i=0; i<titles.length; i++) {
sheet.autoSizeColumn(i);
}
// 6. 设置响应头
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setHeader("Content-Disposition", "attachment;filename=users.xlsx");
workbook.write(response.getOutputStream());
workbook.close();
}
进阶技巧:
- 冻结首行:
sheet.createFreezePane(0, 1, 0, 1) - 数字格式化:
cell.setCellStyle(numberFormatStyle)配合DataFormat类 - 图片插入:
Drawing drawing = sheet.createDrawingPatriarch();
方案2:CSV导出(OpenCSV)
<dependency>
<groupId>com.opencsv</groupId>
<artifactId>opencsv</artifactId>
<version>5.7.1</version>
</dependency>
核心代码实现:
public void exportToCsv(HttpServletResponse response, List<User> dataList) throws Exception {
// 1. 创建CSVWriter
try (OutputStream os = response.getOutputStream();
Writer writer = new OutputStreamWriter(os, StandardCharsets.UTF_8);
CSVWriter csvWriter = new CSVWriter(writer)) {
// 2. 写入表头
String[] header = {"ID", "姓名", "年龄", "注册时间"};
csvWriter.writeNext(header);
// 3. 写入数据
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
for (User user : dataList) {
String[] line = {
String.valueOf(user.getId()),
user.getName(),
String.valueOf(user.getAge()),
df.format(user.getRegisterTime())
};
csvWriter.writeNext(line);
}
}
// 4. 设置响应头
response.setContentType("text/csv;charset=UTF-8");
response.setHeader("Content-Disposition", "attachment;filename=users.csv");
}
特殊处理:
- 字段含逗号:用双引号包裹
"""→"""text,with,commas""" - 换行符处理:启用
quoteChar参数,默认为 - BOM头添加:
writer.write('uFEFF');解决Excel打开乱码
方案3:PDF导出(iText7)
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itext7-core</artifactId>
<version>7.2.3</version>
</dependency>
核心代码实现:
public void exportToPdf(HttpServletResponse response, List<User> dataList) throws IOException {
// 1. 创建PDF文档
PdfWriter writer = new PdfWriter(response.getOutputStream());
PdfDocument pdfDoc = new PdfDocument(writer);
Document doc = new Document(pdfDoc);
// 2. 添加标题
Paragraph title = new Paragraph("用户清单").setFontSize(18).setBold();
doc.add(title);
doc.add(new Paragraph("n")); // 空行
// 3. 创建表格
Table table = new Table(UnitValue.createPercentArray(new float[]{20, 30, 20, 30}));
table.addHeaderCell(createHeaderCell("ID"));
table.addHeaderCell(createHeaderCell("姓名"));
table.addHeaderCell(createHeaderCell("年龄"));
table.addHeaderCell(createHeaderCell("注册时间"));
// 4. 填充数据
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");
for (User user : dataList) {
table.addCell(new Cell().add(new Paragraph(String.valueOf(user.getId()))));
table.addCell(new Cell().add(new Paragraph(user.getName())));
table.addCell(new Cell().add(new Paragraph(String.valueOf(user.getAge()))));
table.addCell(new Cell().add(new Paragraph(df.format(user.getRegisterTime()))));
}
// 5. 添加表格到文档
doc.add(table);
doc.close();
// 6. 设置响应头
response.setContentType("application/pdf");
response.setHeader("Content-Disposition", "attachment;filename=users.pdf");
}
private Cell createHeaderCell(String text) {
Cell cell = new Cell();
cell.add(new Paragraph(text));
cell.setBackgroundColor(DeviceCmyk.MAGENTA);
cell.setTextAlignment(TextAlignment.CENTER);
return cell;
}
排版技巧:
- 跨页表头重复:
table.setHeaderRenderingMode(Table.HEADER_REPEATING); - 自动分页:
doc.setAutoFlush(true);配合table.setKeepTogether(false) - 水印添加:使用
Canvas绘制半透明文字/图片
通用处理逻辑(Controller层)
@GetMapping("/export")
public void exportData(@RequestParam String type, @RequestParam Long id, HttpServletResponse response) {
List<User> dataList = userService.listByDeptId(id); // 根据业务逻辑获取数据
String fileName = "data_" + System.currentTimeMillis() + "." + getFileExtension(type);
try {
switch (type.toLowerCase()) {
case "excel":
exportService.exportToExcel(response, dataList, fileName);
break;
case "csv":
exportService.exportToCsv(response, dataList, fileName);
break;
case "pdf":
exportService.exportToPdf(response, dataList, fileName);
break;
default:
throw new IllegalArgumentException("不支持的导出类型");
}
} catch (Exception e) {
throw new RuntimeException("导出失败", e);
}
}
安全增强措施:
- 文件名校验:
fileName = FilenameUtils.getName(fileName).replaceAll("[^a-zA-Z0-9\u4e00-\u9fa5]", ""); - 防重复提交:使用
@PreventDuplicateSubmit注解或Redis锁 - 权限控制:
@PreAuthorize("hasPermission('data:export')") - 超时机制:
web.xml配置<error-page><error-code>504</error-code></error-page>应对长时间任务
常见问题与解决方案(FAQs)
Q1: 导出十万条数据时出现内存溢出怎么办?
A: 采用以下优化策略组合:
- 流式处理:改用
SXSSFWorkbook(POI提供的低内存版本),设置WritableWorkbook的窗口大小(默认100行)SXSSFWorkbook wb = new SXSSFWorkbook(100); // 只保留100行在内存中
- 分块处理:将数据分成多个Sheet,每个Sheet存储1万条记录
- 延迟加载:使用
CachingRowSetResultSet包装JDBC结果集,按需读取数据 - 垃圾回收:显式调用
System.gc()并在适当位置添加null赋值,帮助JVM回收内存 - 异步导出:通过消息队列(RabbitMQ/Kafka)将导出任务放入后台处理,完成后通知用户下载链接
Q2: 导出的Excel文件打开提示“发现不可读取的内容”?
A: 这是微软Office的安全机制触发,解决方法如下:
- 禁用宏警告:在代码中添加以下元数据(针对.xlsm文件):
PackagePart part = workbook.getPackagePart(); PackageProperties prop = part.getProperties(); prop.setCorrupted(false); // 标记为非损坏文件
- 修复ZIP压缩:确保生成的Excel文件是有效的ZIP压缩包,可用7-Zip验证完整性
- 更新依赖版本:升级POI至最新稳定版(当前推荐5.2.x系列)
- 另存为新文件:建议用户首次打开时选择“另存为”新文件,后续即可正常打开
- 移除无效属性:检查代码中是否错误设置了
CustomProperties或ExtendedProperties,可能导致兼容性问题
性能对比测试(基于10万条数据)
| 指标 | Excel (POI) | CSV (OpenCSV) | PDF (iText7) |
|---|---|---|---|
| 生成时间 | 2s | 1s | 7s |
| 文件大小 | 12MB | 8MB | 5MB |
| 内存峰值 | 450MB | 68MB | 320MB |
| 打开速度 | 中速 | 极快 | 较慢 |
| 二次编辑便利性 |
