外观
报表处理
约 4509 字大约 15 分钟
2025-08-24
一、为什么需要报表处理
在企业级应用开发中,报表功能是不可或缺的核心需求:
- 业务数据统计与分析
- 生成财务报表、销售报表等
- 数据导出与备份
- 与外部系统进行数据交换
而直接使用原生 Java API 处理报表存在明显问题:
- 代码冗长复杂,易出错
- 内存管理困难,大数据量易 OOM
- 样式处理繁琐
- 缺乏统一的处理规范
Spring Boot 结合专业报表工具能够:
- 简化开发:一行代码替代多行原生实现
- 提高性能:优化内存使用,支持大数据量处理
- 统一规范:团队内使用一致的报表处理方式
- 提升用户体验:提供美观、专业的报表输出
二、核心报表技术选型
1. Excel 处理方案对比
| 工具 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Apache POI | 功能全面,社区活跃 | 大数据量易 OOM | 小中型数据量 |
| EasyExcel | 内存友好,API 简洁 | 功能相对较少 | 大数据量导出 |
| JExcelAPI | 轻量级,简单易用 | 已停止维护 | 旧项目兼容 |
| Spire.XLS | 商业库,功能强大 | 需要付费 | 企业级应用 |
2. PDF 处理方案对比
| 工具 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| iText | 功能全面,社区活跃 | 7.0+ 商业授权 | 专业 PDF 生成 |
| JasperReports | 可视化设计,模板化 | 学习曲线较陡 | 复杂报表 |
| Flying Saucer | HTML 转 PDF 简单 | 样式支持有限 | 简单 HTML 转 PDF |
| Apache PDFBox | 开源免费 | API 较底层 | 高级 PDF 操作 |
推荐组合:EasyExcel + iText(开源免费、功能全面、社区活跃)
三、Excel 处理实战
1. EasyExcel(推荐)
为什么选择 EasyExcel
EasyExcel 是阿里巴巴开源的 Excel 处理库,相比 Apache POI 有以下优势:
- 内存友好:基于 SAX 模式解析,极大降低内存消耗
- API 简洁:注解驱动,减少样板代码
- 功能丰富:支持复杂表头、样式、公式等
- 性能优越:大数据量处理速度更快
添加依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.1.1</version>
</dependency>实体类定义
@Data
public class Student {
@ExcelProperty("学生学号")
private String id;
@ExcelProperty("学生姓名")
private String name;
@ExcelProperty(value = "学生性别", converter = GenderConverter.class)
private Integer gender; // 0-女,1-男
@ExcelProperty("学生年龄")
private Integer age;
@ExcelProperty("入学日期")
@DateTimeFormat("yyyy年MM月dd日")
private Date enrollDate;
}自定义转换器(可选)
public class GenderConverter implements Converter<Integer> {
@Override
public Class supportJavaTypeKey() {
return Integer.class;
}
@Override
public CellDataTypeEnum supportExcelTypeKey() {
return CellDataTypeEnum.STRING;
}
@Override
public Integer convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
String genderStr = cellData.getStringValue();
if ("男".equals(genderStr)) {
return 1;
} else if ("女".equals(genderStr)) {
return 0;
}
return null;
}
@Override
public WriteCellData<?> convertToExcelData(Integer value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
WriteCellData<String> cellData = new WriteCellData<>();
if (value != null) {
cellData.setStringValue(value == 1 ? "男" : "女");
}
return cellData;
}
}Excel 导出
@RestController
public class ExcelController {
@GetMapping("/export/students")
public void exportStudents(HttpServletResponse response) throws IOException {
// 设置响应头
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
String fileName = URLEncoder.encode("学生信息", "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
// 创建 ExcelWriter
try (ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream()).build()) {
// 创建 sheet
WriteSheet writeSheet = EasyExcel.writerSheet("学生信息").head(Student.class).build();
// 生成模拟数据
List<Student> students = generateStudents(200);
// 写入数据
excelWriter.write(students, writeSheet);
}
}
private List<Student> generateStudents(int count) {
List<Student> students = new ArrayList<>();
for (int i = 1; i <= count; i++) {
Student student = new Student();
student.setId("1000" + i);
student.setName("姓名" + i);
student.setGender(i % 2);
student.setAge(18 + i % 5);
student.setEnrollDate(new Date(System.currentTimeMillis() - i * 24 * 3600 * 1000L));
students.add(student);
}
return students;
}
}Excel 导入
@PostMapping("/import/students")
public Result importStudents(@RequestParam("file") MultipartFile file) {
try {
// 读取 Excel 数据
List<Student> students = EasyExcel.read(file.getInputStream())
.head(Student.class)
.sheet()
.doReadSync();
// 保存到数据库
studentService.saveBatch(students);
return Result.success("导入成功,共" + students.size() + "条记录");
} catch (Exception e) {
return Result.error("导入失败: " + e.getMessage());
}
}高级功能
1. 自定义样式
// 自定义表头样式
WriteCellStyle headWriteCellStyle = new WriteCellStyle();
headWriteCellStyle.setFillForegroundColor(IndexedColors.LIGHT_BLUE.getIndex());
WriteFont headWriteFont = new WriteFont();
headWriteFont.setFontHeightInPoints((short)12);
headWriteFont.setBold(true);
headWriteCellStyle.setWriteFont(headWriteFont);
// 自定义内容样式
WriteCellStyle contentWriteCellStyle = new WriteCellStyle();
contentWriteCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
// 创建样式策略
HorizontalCellStyleStrategy horizontalCellStyleStrategy =
new HorizontalCellStyleStrategy(headWriteCellStyle, contentWriteCellStyle);
// 写入时应用样式
WriteSheet writeSheet = EasyExcel.writerSheet("学生信息")
.head(Student.class)
.registerWriteHandler(horizontalCellStyleStrategy)
.build();2. 多级表头
// 创建多级表头
List<List<String>> head = new ArrayList<>();
head.add(Arrays.asList("基本信息"));
head.add(Arrays.asList("学生学号"));
head.add(Arrays.asList("学生姓名"));
head.add(Arrays.asList("学生性别"));
head.add(Arrays.asList("学生年龄"));
head.add(Arrays.asList("入学日期"));
// 写入时使用自定义表头
WriteSheet writeSheet = EasyExcel.writerSheet("学生信息")
.head(head)
.registerWriteHandler(new SimpleColumnWidthStyleStrategy(15))
.build();3. 大数据量导出(分页)
@GetMapping("/export/large-students")
public void exportLargeStudents(HttpServletResponse response) {
// 设置响应头
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
String fileName = URLEncoder.encode("大数据量学生信息", "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
try (ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream()).build()) {
WriteSheet writeSheet = EasyExcel.writerSheet("学生信息").head(Student.class).build();
// 分页查询写入
int pageSize = 1000;
int pageNo = 1;
List<Student> students;
do {
students = studentService.getStudentsByPage(pageNo, pageSize);
if (!students.isEmpty()) {
excelWriter.write(students, writeSheet);
pageNo++;
}
} while (!students.isEmpty());
} catch (Exception e) {
log.error("大数据量导出失败", e);
}
}2. Apache POI(备选)
为什么选择 POI
- 功能更全面:支持更复杂的 Excel 操作
- 社区更成熟:文档和案例更丰富
- 兼容性更好:支持旧版 Excel 格式
添加依赖
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.2.3</version>
</dependency>Excel 导出示例
@GetMapping("/export/poi/students")
public void exportStudentsWithPOI(HttpServletResponse response) throws IOException {
// 设置响应头
response.setContentType("application/vnd.ms-excel");
response.setCharacterEncoding("utf-8");
String fileName = URLEncoder.encode("学生信息-POI", "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".xls");
// 创建工作簿
Workbook workbook = new HSSFWorkbook();
// 创建工作表
Sheet sheet = workbook.createSheet("学生信息");
// 创建表头
Row headerRow = sheet.createRow(0);
String[] headers = {"学生学号", "学生姓名", "学生性别", "学生年龄", "入学日期"};
for (int i = 0; i < headers.length; i++) {
headerRow.createCell(i).setCellValue(headers[i]);
}
// 生成数据
List<Student> students = generateStudents(200);
// 填充数据
int rowNum = 1;
for (Student student : students) {
Row row = sheet.createRow(rowNum++);
row.createCell(0).setCellValue(student.getId());
row.createCell(1).setCellValue(student.getName());
row.createCell(2).setCellValue(student.getGender() == 1 ? "男" : "女");
row.createCell(3).setCellValue(student.getAge());
row.createCell(4).setCellValue(new SimpleDateFormat("yyyy-MM-dd").format(student.getEnrollDate()));
}
// 调整列宽
for (int i = 0; i < headers.length; i++) {
sheet.autoSizeColumn(i);
}
// 输出
workbook.write(response.getOutputStream());
workbook.close();
}四、PDF 报表生成
1. iText 7(推荐)
为什么选择 iText 7
- 功能全面:支持复杂 PDF 生成
- 开源免费:社区版完全免费
- 文档丰富:官方文档详细
- 性能良好:处理速度快
添加依赖
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>kernel</artifactId>
<version>8.0.1</version>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>io</artifactId>
<version>8.0.1</version>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>layout</artifactId>
<version>8.0.1</version>
</dependency>PDF 生成示例
@GetMapping("/export/student/pdf")
public void exportStudentPdf(HttpServletResponse response) throws IOException {
// 设置响应头
response.setContentType("application/pdf");
response.setCharacterEncoding("utf-8");
String fileName = URLEncoder.encode("学生信息", "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".pdf");
// 创建 PDF 文档
PdfDocument pdfDocument = new PdfDocument(new PdfWriter(response.getOutputStream()));
Document document = new Document(pdfDocument);
// 添加标题
Paragraph title = new Paragraph("学生信息表")
.setFontSize(18)
.setBold()
.setTextAlignment(TextAlignment.CENTER)
.setMarginBottom(20);
document.add(title);
// 创建表格
Table table = new Table(new float[]{2, 3, 2, 2, 3});
table.setWidth(UnitValue.createPercentValue(100));
// 添加表头
table.addHeaderCell(new Cell().add(new Paragraph("学号").setBold()));
table.addHeaderCell(new Cell().add(new Paragraph("姓名").setBold()));
table.addHeaderCell(new Cell().add(new Paragraph("性别").setBold()));
table.addHeaderCell(new Cell().add(new Paragraph("年龄").setBold()));
table.addHeaderCell(new Cell().add(new Paragraph("入学日期").setBold()));
// 生成数据
List<Student> students = generateStudents(200);
// 填充数据
for (Student student : students) {
table.addCell(new Cell().add(new Paragraph(student.getId())));
table.addCell(new Cell().add(new Paragraph(student.getName())));
table.addCell(new Cell().add(new Paragraph(student.getGender() == 1 ? "男" : "女")));
table.addCell(new Cell().add(new Paragraph(String.valueOf(student.getAge()))));
table.addCell(new Cell().add(new Paragraph(new SimpleDateFormat("yyyy-MM-dd").format(student.getEnrollDate()))));
}
// 添加表格到文档
document.add(table);
// 添加页脚
document.add(new Paragraph("生成日期: " + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()))
.setTextAlignment(TextAlignment.RIGHT)
.setMarginTop(20));
// 关闭文档
document.close();
}2. HTML 转 PDF(Flying Saucer)
为什么选择 Flying Saucer
- 简单易用:直接将 HTML 转为 PDF
- 样式保留:支持 CSS 样式
- 开发快速:前端熟悉 HTML 即可
添加依赖
<dependency>
<groupId>org.xhtmlrenderer</groupId>
<artifactId>flying-saucer-pdf-itext5</artifactId>
<version>9.1.20</version>
</dependency>HTML 转 PDF 示例
@GetMapping("/export/student/html-pdf")
public void exportStudentHtmlPdf(HttpServletResponse response) throws Exception {
// 设置响应头
response.setContentType("application/pdf");
response.setCharacterEncoding("utf-8");
String fileName = URLEncoder.encode("学生信息", "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".pdf");
// 生成 HTML 内容
String htmlContent = generateStudentHtml();
// 将 HTML 转为 PDF
ITextRenderer renderer = new ITextRenderer();
renderer.setDocumentFromString(htmlContent);
renderer.layout();
renderer.createPDF(response.getOutputStream());
}
private String generateStudentHtml() {
// 生成模拟数据
List<Student> students = generateStudents(20);
// 构建 HTML
StringBuilder html = new StringBuilder();
html.append("<!DOCTYPE html>");
html.append("<html>");
html.append("<head>");
html.append("<meta charset='UTF-8'>");
html.append("<style>");
html.append("table { width: 100%; border-collapse: collapse; }");
html.append("th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }");
html.append("th { background-color: #f2f2f2; }");
html.append("tr:nth-child(even) { background-color: #f9f9f9; }");
html.append("h1 { text-align: center; color: #333; }");
html.append(".footer { text-align: right; margin-top: 20px; color: #666; }");
html.append("</style>");
html.append("</head>");
html.append("<body>");
html.append("<h1>学生信息表</h1>");
html.append("<table>");
html.append("<tr><th>学号</th><th>姓名</th><th>性别</th><th>年龄</th><th>入学日期</th></tr>");
for (Student student : students) {
html.append("<tr>");
html.append("<td>").append(student.getId()).append("</td>");
html.append("<td>").append(student.getName()).append("</td>");
html.append("<td>").append(student.getGender() == 1 ? "男" : "女").append("</td>");
html.append("<td>").append(student.getAge()).append("</td>");
html.append("<td>").append(new SimpleDateFormat("yyyy-MM-dd").format(student.getEnrollDate())).append("</td>");
html.append("</tr>");
}
html.append("</table>");
html.append("<div class='footer'>生成日期: ").append(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())).append("</div>");
html.append("</body>");
html.append("</html>");
return html.toString();
}五、报表优化技巧
1. 内存优化
大数据量导出优化
@GetMapping("/export/optimized/students")
public void exportOptimizedStudents(HttpServletResponse response) {
// 设置响应头
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
String fileName = URLEncoder.encode("优化版学生信息", "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
try (ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream()).build()) {
WriteSheet writeSheet = EasyExcel.writerSheet("学生信息").head(Student.class).build();
// 使用分页查询避免一次性加载所有数据
int pageSize = 5000;
int pageNo = 1;
List<Student> students;
do {
students = studentService.getStudentsByPage(pageNo, pageSize);
if (!students.isEmpty()) {
excelWriter.write(students, writeSheet);
// 手动释放内存
students.clear();
System.gc();
pageNo++;
}
} while (!students.isEmpty());
} catch (Exception e) {
log.error("大数据量导出失败", e);
}
}内存监控与调整
// 在启动脚本中设置JVM参数
// -Xms512m -Xmx1024m -XX:+UseG1GC2. 性能优化
并行处理
@GetMapping("/export/parallel/students")
public void exportParallelStudents(HttpServletResponse response) {
// 设置响应头
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
String fileName = URLEncoder.encode("并行处理学生信息", "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
try (ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream()).build()) {
WriteSheet writeSheet = EasyExcel.writerSheet("学生信息").head(Student.class).build();
// 获取总记录数
long total = studentService.countStudents();
int pageSize = 10000;
int totalPages = (int) Math.ceil((double) total / pageSize);
// 使用并行流处理
List<CompletableFuture<List<Student>>> futures = IntStream.range(0, totalPages)
.mapToObj(page -> CompletableFuture.supplyAsync(() ->
studentService.getStudentsByPage(page + 1, pageSize)))
.collect(Collectors.toList());
// 等待所有任务完成并写入
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
for (CompletableFuture<List<Student>> future : futures) {
excelWriter.write(future.get(), writeSheet);
}
} catch (Exception e) {
log.error("并行导出失败", e);
}
}异步导出
@Service
public class ExcelExportService {
@Async
public CompletableFuture<File> generateStudentExcel() {
try {
// 创建临时文件
File tempFile = File.createTempFile("students_", ".xlsx");
// 写入数据
try (ExcelWriter excelWriter = EasyExcel.write(tempFile).build()) {
WriteSheet writeSheet = EasyExcel.writerSheet("学生信息").head(Student.class).build();
List<Student> students = studentService.getAllStudents();
excelWriter.write(students, writeSheet);
}
return CompletableFuture.completedFuture(tempFile);
} catch (Exception e) {
throw new RuntimeException("生成Excel失败", e);
}
}
}
@RestController
public class ExportController {
@Autowired
private ExcelExportService excelExportService;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@GetMapping("/export/async/students")
public Result exportAsyncStudents() {
// 生成唯一任务ID
String taskId = UUID.randomUUID().toString();
// 提交异步任务
CompletableFuture<File> future = excelExportService.generateStudentExcel();
// 将任务状态存入Redis
redisTemplate.opsForValue().set("export:task:" + taskId, "PROCESSING", 30, TimeUnit.MINUTES);
// 监听任务完成
future.thenAccept(file -> {
redisTemplate.opsForValue().set("export:task:" + taskId, "SUCCESS", 24, TimeUnit.HOURS);
redisTemplate.opsForValue().set("export:result:" + taskId, file.getAbsolutePath(), 24, TimeUnit.HOURS);
}).exceptionally(ex -> {
redisTemplate.opsForValue().set("export:task:" + taskId, "FAILED", 1, TimeUnit.HOURS);
return null;
});
return Result.success(Collections.singletonMap("taskId", taskId));
}
@GetMapping("/export/async/status/{taskId}")
public Result getExportStatus(@PathVariable String taskId) {
String status = (String) redisTemplate.opsForValue().get("export:task:" + taskId);
if (status == null) {
return Result.error("任务不存在或已过期");
}
return Result.success(Collections.singletonMap("status", status));
}
@GetMapping("/export/async/download/{taskId}")
public void downloadExportedFile(@PathVariable String taskId, HttpServletResponse response) {
String filePath = (String) redisTemplate.opsForValue().get("export:result:" + taskId);
if (filePath == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
File file = new File(filePath);
if (!file.exists()) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
// 设置响应头
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
String fileName = URLEncoder.encode("学生信息", "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
// 输出文件
try (FileInputStream fis = new FileInputStream(file);
OutputStream os = response.getOutputStream()) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
os.write(buffer, 0, bytesRead);
}
} catch (IOException e) {
log.error("下载文件失败", e);
}
}
}3. 样式优化
EasyExcel 样式设置
@GetMapping("/export/styled/students")
public void exportStyledStudents(HttpServletResponse response) {
// 设置响应头
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
String fileName = URLEncoder.encode("样式优化学生信息", "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
try (ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream()).build()) {
// 创建样式策略
WriteCellStyle headWriteCellStyle = new WriteCellStyle();
headWriteCellStyle.setFillForegroundColor(IndexedColors.LIGHT_BLUE.getIndex());
WriteFont headWriteFont = new WriteFont();
headWriteFont.setFontHeightInPoints((short) 12);
headWriteFont.setBold(true);
headWriteCellStyle.setWriteFont(headWriteFont);
WriteCellStyle contentWriteCellStyle = new WriteCellStyle();
contentWriteCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
contentWriteCellStyle.setWrapped(true); // 自动换行
// 设置边框
contentWriteCellStyle.setBorderBottom(BorderStyle.THIN);
contentWriteCellStyle.setBorderLeft(BorderStyle.THIN);
contentWriteCellStyle.setBorderRight(BorderStyle.THIN);
contentWriteCellStyle.setBorderTop(BorderStyle.THIN);
// 创建样式策略
HorizontalCellStyleStrategy horizontalCellStyleStrategy =
new HorizontalCellStyleStrategy(headWriteCellStyle, contentWriteCellStyle);
// 创建表头样式
List<List<String>> head = new ArrayList<>();
head.add(Arrays.asList("基本信息", "", "", "", ""));
head.add(Arrays.asList("学生学号", "学生姓名", "学生性别", "学生年龄", "入学日期"));
// 创建列宽策略
AbstractColumnWidthStyleStrategy columnWidthStrategy = new AbstractColumnWidthStyleStrategy() {
@Override
protected int getColumnWidth(WriteCellData<?> cellData, List<WriteCellData<?>> cellDataList, Cell cell, Head head,
Integer relativeRowIndex, Boolean isHead) {
return 20; // 固定列宽
}
};
// 写入数据
WriteSheet writeSheet = EasyExcel.writerSheet("学生信息")
.head(head)
.registerWriteHandler(horizontalCellStyleStrategy)
.registerWriteHandler(columnWidthStrategy)
.build();
List<Student> students = generateStudents(200);
excelWriter.write(students, writeSheet);
} catch (Exception e) {
log.error("样式优化导出失败", e);
}
}六、常见问题与解决方案
1. 中文乱码问题
问题现象:导出的 Excel 或 PDF 中中文显示为乱码
解决方案:
EasyExcel 中文乱码
// 设置编码为 UTF-8
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
String fileName = URLEncoder.encode("中文文件名", "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");iText 中文乱码
// 添加中文字体支持
PdfFont font = PdfFontFactory.createFont("STSong-Light", "UniGB-UCS2-H", true);
Document document = new Document(pdfDocument);
document.setFont(font);2. 内存溢出(OOM)问题
问题现象:处理大数据量报表时出现 java.lang.OutOfMemoryError
解决方案:
- 使用 EasyExcel 替代 POI
- 分页查询,避免一次性加载所有数据
- 手动释放内存
// 分页查询示例
List<Student> students = studentService.getStudentsByPage(pageNo, pageSize);
excelWriter.write(students, writeSheet);
students.clear(); // 手动释放
System.gc(); // 建议JVM进行垃圾回收- 调整 JVM 参数
-Xms512m -Xmx2048m -XX:+UseG1GC3. 样式丢失问题
问题现象:导出的 Excel 样式与预期不符
解决方案:
EasyExcel 样式设置
// 创建样式策略
WriteCellStyle headWriteCellStyle = new WriteCellStyle();
headWriteCellStyle.setFillForegroundColor(IndexedColors.LIGHT_BLUE.getIndex());
// ...其他样式设置
WriteCellStyle contentWriteCellStyle = new WriteCellStyle();
contentWriteCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
// ...其他样式设置
HorizontalCellStyleStrategy styleStrategy = new HorizontalCellStyleStrategy(headWriteCellStyle, contentWriteCellStyle);
WriteSheet writeSheet = EasyExcel.writerSheet("学生信息")
.head(Student.class)
.registerWriteHandler(styleStrategy)
.build();4. 日期格式问题
问题现象:日期显示为数字而非日期格式
解决方案:
EasyExcel 日期格式
// 在实体类中使用注解
@DateTimeFormat("yyyy年MM月dd日")
private Date enrollDate;
// 或在写入时指定格式
WriteSheet writeSheet = EasyExcel.writerSheet("学生信息")
.head(Student.class)
.registerWriteHandler(new CustomCellWriteHandler())
.build();
// 自定义处理器
public class CustomCellWriteHandler implements CellWriteHandler {
@Override
public void afterCellDispose(WriteSheetContext context) {
if (context.getColumnIndex() == 4 && context.getRowIndex() > 0) { // 第5列是日期
CellData cellData = context.getFirstCellData();
if (cellData.getType() == CellDataTypeEnum.DATE) {
cellData.setFormulaStr("TEXT(" + cellData.getNumberValue() + ",\"yyyy-mm-dd\")");
}
}
}
}七、最佳实践总结
1. 技术选型原则
- Excel 处理:优先选择 EasyExcel,除非有特殊需求需要 POI
- PDF 生成:简单报表用 Flying Saucer,复杂报表用 iText
- 大数据量:必须考虑内存优化,避免 OOM
- 国际化支持:确保报表支持多语言
2. 开发规范
- 统一响应格式:所有导出接口应返回一致的响应结构
- 异步处理:大数据量报表应采用异步导出
- 任务状态跟踪:提供任务状态查询接口
- 文件清理机制:临时文件应及时清理
- 错误处理:完善的异常处理和日志记录
3. 性能优化建议
| 优化点 | 推荐做法 |
|---|---|
| 内存管理 | 分页查询、手动释放内存、使用 SAX 模式 |
| 处理速度 | 并行处理、缓存重复数据、减少 IO 操作 |
| 网络传输 | GZIP 压缩、分块传输、CDN 加速 |
| 用户体验 | 异步导出、进度反馈、预览功能 |
4. 安全注意事项
文件名安全:防止路径遍历攻击
String safeFileName = URLEncoder.encode(fileName, "UTF-8").replaceAll("\\+", "%20");内容安全:过滤特殊字符,防止 XSS 攻击
String safeContent = StringEscapeUtils.escapeHtml4(content);权限控制:确保只有授权用户可以导出数据
@PreAuthorize("hasRole('ADMIN')") @GetMapping("/export/students") public void exportStudents(...) { ... }敏感数据:避免导出包含敏感信息的报表
// 在导出前过滤敏感字段 students.forEach(s -> s.setPassword(null));
八、完整示例:学生信息报表系统
1. 项目结构
report-system
├── src/main/java
│ ├── controller
│ │ └── ReportController.java # 报表控制器
│ ├── service
│ │ ├── ReportService.java # 报表服务
│ │ └── ExcelExportService.java # Excel导出服务
│ ├── entity
│ │ └── Student.java # 学生实体
│ ├── converter
│ │ └── GenderConverter.java # 性别转换器
│ └── config
│ └── AsyncConfig.java # 异步配置
└── src/main/resources
└── application.yml # 配置文件2. 核心代码
ReportController.java:
@RestController
@RequestMapping("/api/reports")
public class ReportController {
@Autowired
private ReportService reportService;
@GetMapping("/students/excel")
public void exportStudentsExcel(HttpServletResponse response) {
reportService.exportStudentsExcel(response);
}
@GetMapping("/students/pdf")
public void exportStudentsPdf(HttpServletResponse response) {
reportService.exportStudentsPdf(response);
}
@PostMapping("/students/import")
public Result importStudents(@RequestParam("file") MultipartFile file) {
return reportService.importStudents(file);
}
@GetMapping("/students/async/excel")
public Result exportStudentsAsync() {
return reportService.exportStudentsAsync();
}
@GetMapping("/students/async/status/{taskId}")
public Result getExportStatus(@PathVariable String taskId) {
return reportService.getExportStatus(taskId);
}
@GetMapping("/students/async/download/{taskId}")
public void downloadExportedFile(@PathVariable String taskId, HttpServletResponse response) {
reportService.downloadExportedFile(taskId, response);
}
}ReportService.java:
@Service
public class ReportService {
@Autowired
private StudentService studentService;
@Autowired
private ExcelExportService excelExportService;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public void exportStudentsExcel(HttpServletResponse response) {
// 实现Excel导出
try {
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
String fileName = URLEncoder.encode("学生信息", "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
try (ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream()).build()) {
WriteSheet writeSheet = EasyExcel.writerSheet("学生信息").head(Student.class).build();
List<Student> students = studentService.getAllStudents();
excelWriter.write(students, writeSheet);
}
} catch (Exception e) {
throw new RuntimeException("Excel导出失败", e);
}
}
public void exportStudentsPdf(HttpServletResponse response) {
// 实现PDF导出
try {
response.setContentType("application/pdf");
response.setCharacterEncoding("utf-8");
String fileName = URLEncoder.encode("学生信息", "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".pdf");
PdfDocument pdfDocument = new PdfDocument(new PdfWriter(response.getOutputStream()));
Document document = new Document(pdfDocument);
// 添加内容...
document.close();
} catch (Exception e) {
throw new RuntimeException("PDF生成失败", e);
}
}
public Result importStudents(MultipartFile file) {
try {
List<Student> students = EasyExcel.read(file.getInputStream())
.head(Student.class)
.sheet()
.doReadSync();
studentService.saveBatch(students);
return Result.success("导入成功,共" + students.size() + "条记录");
} catch (Exception e) {
return Result.error("导入失败: " + e.getMessage());
}
}
public Result exportStudentsAsync() {
String taskId = UUID.randomUUID().toString();
CompletableFuture<File> future = excelExportService.generateStudentExcel();
redisTemplate.opsForValue().set("export:task:" + taskId, "PROCESSING", 30, TimeUnit.MINUTES);
future.thenAccept(file -> {
redisTemplate.opsForValue().set("export:task:" + taskId, "SUCCESS", 24, TimeUnit.HOURS);
redisTemplate.opsForValue().set("export:result:" + taskId, file.getAbsolutePath(), 24, TimeUnit.HOURS);
}).exceptionally(ex -> {
redisTemplate.opsForValue().set("export:task:" + taskId, "FAILED", 1, TimeUnit.HOURS);
return null;
});
return Result.success(Collections.singletonMap("taskId", taskId));
}
public Result getExportStatus(String taskId) {
String status = (String) redisTemplate.opsForValue().get("export:task:" + taskId);
if (status == null) {
return Result.error("任务不存在或已过期");
}
return Result.success(Collections.singletonMap("status", status));
}
public void downloadExportedFile(String taskId, HttpServletResponse response) {
String filePath = (String) redisTemplate.opsForValue().get("export:result:" + taskId);
if (filePath == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
File file = new File(filePath);
if (!file.exists()) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
// 设置响应头
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
String fileName = URLEncoder.encode("学生信息", "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
// 输出文件
try (FileInputStream fis = new FileInputStream(file);
OutputStream os = response.getOutputStream()) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
os.write(buffer, 0, bytesRead);
}
} catch (IOException e) {
throw new RuntimeException("下载文件失败", e);
}
}
}九、总结
本文详细介绍了 Spring Boot 中报表处理的核心技术与最佳实践,覆盖了 80% 的常见应用场景:
- Excel 处理:重点介绍了 EasyExcel 的使用,包括导出、导入、样式设置等
- PDF 生成:讲解了 iText 和 HTML 转 PDF 两种主流方案
- 性能优化:提供了内存优化、异步处理等实用技巧
- 常见问题:解决了中文乱码、OOM、样式丢失等常见问题
关键收获:
- 优先选择 EasyExcel 处理 Excel,避免内存问题
- 大数据量报表必须采用分页查询和异步处理
- 统一报表导出接口规范,提升用户体验
- 重视报表安全,防止敏感数据泄露
