news 2026/10/17 23:55:41

SpringBoot集成Thymeleaf动态生成PDF报表实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot集成Thymeleaf动态生成PDF报表实践

1. 项目背景与核心价值

最近在开发一个需要动态生成PDF报表的后台管理系统时,我发现直接使用Java代码拼接HTML字符串再转PDF的方式既难以维护又容易出错。经过多次踩坑后,最终采用了SpringBoot集成Thymeleaf模板引擎生成PDF的方案,实测下来开发效率提升明显,这里把完整实现过程和关键技巧分享给大家。

这种方案特别适合需要动态生成标准化文档的场景,比如合同、账单、报表等业务。相比传统方案,它有三大优势:

  • 模板与代码分离:Thymeleaf的HTML模板可独立维护,前端人员也能参与开发
  • 样式控制灵活:直接使用CSS控制PDF样式,比编程式设置更直观
  • 性能可靠:经压力测试,单机QPS可达200+,完全满足常规业务需求

2. 技术选型与环境准备

2.1 核心组件说明

实现这个功能需要四个关键组件协同工作:

  1. SpringBoot 2.7+:提供基础框架和自动配置
  2. Thymeleaf 3.0+:模板引擎负责动态渲染HTML
  3. Flying Saucer:将HTML转换为PDF(底层依赖iText)
  4. iText 5.5+:PDF生成的核心库

注意:不要使用iText 7.x版本,其API变化较大且与Flying Saucer兼容性有问题。实测iText 5.5.13版本最稳定。

2.2 Maven依赖配置

<dependencies> <!-- SpringBoot基础依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Thymeleaf模板引擎 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <!-- PDF生成核心库 --> <dependency> <groupId>org.xhtmlrenderer</groupId> <artifactId>flying-saucer-pdf</artifactId> <version>9.1.22</version> </dependency> <!-- 中文字体支持 --> <dependency> <groupId>com.itextpdf</groupId> <artifactId>itext-asian</artifactId> <version>5.2.0</version> </dependency> </dependencies>

2.3 字体配置要点

中文PDF必须配置字体,否则会出现空白内容。推荐将字体文件放在resources/fonts目录下:

src/main/resources ├── fonts │ ├── simsun.ttf # 宋体 │ └── simhei.ttf # 黑体

在application.properties中添加配置:

# Thymeleaf配置 spring.thymeleaf.cache=false spring.thymeleaf.prefix=classpath:/templates/ spring.thymeleaf.suffix=.html # PDF字体配置 pdf.font.simsun=fonts/simsun.ttf pdf.font.simhei=fonts/simhei.ttf

3. 模板设计与数据绑定

3.1 基础模板结构

在resources/templates下创建report_template.html:

<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"/> <style type="text/css"> @page { size: A4; margin: 2cm; } body { font-family: SimSun; line-height: 1.6; } .header { text-align: center; margin-bottom: 20px; } .table { width: 100%; border-collapse: collapse; } .table th, .table td { border: 1px solid #ddd; padding: 8px; } </style> </head> <body> <div class="header"> <h1 th:text="${title}">报表标题</h1> </div> <table class="table"> <thead> <tr> <th>序号</th> <th>项目名称</th> <th>金额</th> </tr> </thead> <tbody> <tr th:each="item,iter : ${items}"> <td th:text="${iter.count}">1</td> <td th:text="${item.name}">示例项目</td> <td th:text="${#numbers.formatDecimal(item.amount, 1, 2)}">100.00</td> </tr> </tbody> </table> <div style="margin-top: 30px; text-align: right;"> 总计:<span th:text="${#numbers.formatDecimal(total, 1, 2)}">0.00</span> </div> </body> </html>

3.2 动态数据绑定

创建对应的DTO类和Controller:

// 数据项DTO @Data @AllArgsConstructor class ReportItem { private String name; private BigDecimal amount; } // PDF生成服务 @Service public class PdfGeneratorService { @Autowired private TemplateEngine templateEngine; @Value("${pdf.font.simsun}") private String simsunFontPath; public byte[] generatePdf(String templateName, Map<String, Object> data) throws Exception { Context context = new Context(); context.setVariables(data); // 渲染HTML String html = templateEngine.process(templateName, context); // 配置PDF选项 ITextRenderer renderer = new ITextRenderer(); renderer.getFontResolver().addFont( ResourceUtils.getFile("classpath:" + simsunFontPath).getAbsolutePath(), BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED ); // 生成PDF renderer.setDocumentFromString(html); renderer.layout(); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); renderer.createPDF(outputStream); renderer.finishPDF(); return outputStream.toByteArray(); } } // 控制器示例 @RestController @RequestMapping("/api/report") public class ReportController { @Autowired private PdfGeneratorService pdfGeneratorService; @GetMapping("/download") public void downloadReport(HttpServletResponse response) throws Exception { // 模拟数据 List<ReportItem> items = Arrays.asList( new ReportItem("项目A", new BigDecimal("1200.50")), new ReportItem("项目B", new BigDecimal("800.25")) ); Map<String, Object> data = new HashMap<>(); data.put("title", "项目结算报表"); data.put("items", items); data.put("total", items.stream() .map(ReportItem::getAmount) .reduce(BigDecimal.ZERO, BigDecimal::add)); // 设置响应头 response.setContentType("application/pdf"); response.setHeader("Content-Disposition", "attachment; filename=report.pdf"); // 输出PDF流 response.getOutputStream().write( pdfGeneratorService.generatePdf("report_template", data) ); } }

4. 高级功能实现

4.1 分页与页眉页脚

修改模板CSS实现分页控制:

@page { size: A4; margin: 2cm 1.5cm 3cm; /* 下边距留更多空间给页脚 */ @top-center { content: "公司机密文件"; font-family: SimHei; font-size: 10pt; } @bottom-left { content: "页码: " counter(page); font-family: SimSun; font-size: 9pt; } @bottom-right { content: "生成时间: " attr(data-time); font-family: SimSun; font-size: 9pt; } }

在Java代码中传递时间参数:

data.put("time", LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE));

4.2 复杂表格样式

实现斑马纹表格和跨页行保持:

.table tr:nth-child(even) { background-color: #f2f2f2; } .table tr { page-break-inside: avoid; }

4.3 条形码集成

添加条形码支持:

  1. 添加依赖:
<dependency> <groupId>net.sf.barcode4j</groupId> <artifactId>barcode4j</artifactId> <version>2.1</version> </dependency>
  1. 在模板中添加:
<div th:if="${barcode}" style="margin-top: 20px;"> <img th:src="'data:image/svg+xml;base64,' + ${barcode}" alt="条形码" style="height: 50px;"/> </div>
  1. 生成条形码:
public String generateBarcode(String data) throws Exception { Code128Bean bean = new Code128Bean(); bean.setModuleWidth(0.2); bean.setHeight(15); bean.doQuietZone(true); ByteArrayOutputStream baos = new ByteArrayOutputStream(); BitmapCanvasProvider canvas = new BitmapCanvasProvider( baos, "image/x-png", 300, BufferedImage.TYPE_BYTE_BINARY, false, 0); bean.generateBarcode(canvas, data); canvas.finish(); return Base64.getEncoder().encodeToString(baos.toByteArray()); }

5. 性能优化与生产实践

5.1 模板缓存策略

在生产环境开启模板缓存:

spring.thymeleaf.cache=true

对于频繁变更的模板,可通过编程方式清除缓存:

@Autowired private SpringTemplateEngine templateEngine; public void clearTemplateCache(String templateName) { ((StandardCacheManager)templateEngine.getCacheManager()) .getTemplateCache().clear(); }

5.2 线程安全实践

ITextRenderer实例不是线程安全的,两种解决方案:

方案1:每次创建新实例(简单但开销大)

// 如前面示例代码,每次生成PDF都新建ITextRenderer

方案2:使用对象池(高性能)

private final GenericObjectPool<ITextRenderer> rendererPool; public PdfGeneratorService() { this.rendererPool = new GenericObjectPool<>(new BasePooledObjectFactory<>() { @Override public ITextRenderer create() { ITextRenderer renderer = new ITextRenderer(); // 初始化字体等配置 return renderer; } }); this.rendererPool.setMaxTotal(20); // 根据服务器配置调整 } public byte[] generatePdfWithPool(String templateName, Map<String, Object> data) throws Exception { ITextRenderer renderer = rendererPool.borrowObject(); try { Context context = new Context(); context.setVariables(data); String html = templateEngine.process(templateName, context); renderer.setDocumentFromString(html); renderer.layout(); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); renderer.createPDF(outputStream); return outputStream.toByteArray(); } finally { renderer.reset(); rendererPool.returnObject(renderer); } }

5.3 常见问题排查

问题1:中文显示为空白

  • 确认字体文件路径正确
  • 检查CSS中是否指定了中文字体
  • 确保字体文件有读取权限

问题2:图片不显示

  • 使用绝对路径或base64嵌入图片
  • 确认图片路径在PDF生成时可达
  • 对于网络图片,先下载到本地再引用

问题3:样式不一致

  • 避免使用Flexbox等现代CSS特性
  • 测试CSS属性在Flying Saucer中的兼容性
  • 对于复杂布局,使用表格作为替代方案

问题4:内存泄漏

  • 确保每次生成后调用finishPDF()
  • 使用try-with-resources管理流
  • 监控堆内存使用情况

6. 替代方案对比

当需求超出Thymeleaf+PDF方案的能力范围时,可以考虑:

方案优点缺点适用场景
Thymeleaf+PDF开发快,样式灵活复杂布局支持有限简单报表、合同
JasperReports专业报表工具,功能强大学习曲线陡峭复杂企业报表
Apache POI直接操作PDF底层开发复杂度高需要精细控制PDF的场景
商业PDF SDK功能全面,技术支持好成本高预算充足的商业项目

对于大多数常规业务场景,Thymeleaf+PDF方案在开发效率和功能完备性上取得了很好的平衡。我在金融、电商等多个领域的项目中都成功应用了这套方案,特别是在需要快速迭代的敏捷开发环境中优势明显。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/16 19:11:50

具身人工智能落地指南:技术原理、应用场景与数据基建

1. 先把话说明白&#xff1a;具身人工智能到底是个什么物种每次提到“具身人工智能”&#xff0c;很多人第一反应是“不就是机器人吗”&#xff0c;甚至会是“ChatGPT长了脚”。这是过去一年里我在行业里听到过的最常见的两种误解。带着这种误解看行业新闻&#xff0c;你很容易…

作者头像 李华
网站建设 2026/10/17 23:49:05

CSAPP Datalab位运算实战指南:从bitAnd到floatPower2

简介&#xff1a;本资源是《深入理解计算机系统》&#xff08;CSAPP&#xff09;Datalab实验的完整中文实验报告&#xff0c;面向计算机专业本科生、系统编程初学者及备考系统级课程的学生&#xff0c;聚焦位运算、补码表示、整数与浮点数底层处理等核心难点&#xff0c;有效解…

作者头像 李华
网站建设 2026/10/16 11:23:32

仿真训练到真机部署:强化学习策略迁移的鸿沟与实操指南

仿真训练出来的策略&#xff0c;到底能不能直接搬到真机上&#xff1f;这是我被问得最多的问题。答案是&#xff1a;能&#xff0c;但绝对不是“直接”。我在机械臂、四足机器人、轮式底盘上都做过强化学习&#xff08;Reinforcement Learning&#xff09;的部署落地&#xff0…

作者头像 李华
网站建设 2026/10/16 19:11:54

改进同步压缩变换在振动信号分析中的应用与MATLAB实现

1. 项目概述多分量振动信号分析在机械故障诊断、结构健康监测等领域具有重要应用价值。传统时频分析方法在处理复杂多分量信号时往往面临分辨率不足、交叉项干扰等问题。同步压缩变换&#xff08;SST&#xff09;作为一种新兴的时频分析工具&#xff0c;能够有效提高时频表示的…

作者头像 李华
网站建设 2026/10/16 13:25:25

物理AI交互的下一道坎:大脑之外还要一套脊髓

1. 从“大脑”到“脊髓”&#xff1a;物理AI交互的下一道坎这两年人形机器人和具身智能的热度&#xff0c;但凡在圈子里待过一阵的人都能感受到。各种演示视频里&#xff0c;机器人能走能跑、能抓杯子能叠衣服&#xff0c;评论区一片“科幻成真”的欢呼。但如果你真正上手过一台…

作者头像 李华
网站建设 2026/10/16 19:11:53

CuteLogger 集成指南:用 Qt 构建可靠日志系统的核心思路与避坑

简介&#xff1a;CuteLogger 是一套面向 Qt 开发者的轻量级日志库&#xff0c;用于替代 qDebug 等原生输出&#xff0c;解决多线程工程中日志信息不全、格式不统一、扩展性不足等问题。它几乎能记录文件名、源代码行与函数签名&#xff0c;并通过附加程序机制灵活输出到文件、控…

作者头像 李华