Spring Boot + Flying Saucer + Thymeleaf PDF 完整指南
在实际开发中,PDF生成是常见需求,如报表导出、订单凭证、合同生成等。本文将详细讲解如何基于 Spring Boot + Flying Saucer + Thymeleaf 实现 PDF 生成,涵盖技术方案解析、核心注解与方法、项目搭建、关键问题解决、功能扩展等关键内容,新手也能快速上手。
本文核心亮点:
-
清晰层次结构:从技术解析到实操落地,逻辑循序渐进
-
核心要点突出:重点讲解核心注解、方法及使用场景
-
代码可复用:提供封装好的工具类,直接复制可用
-
问题导向:针对性解决中文乱码、样式兼容等核心痛点
一、技术方案概述
1.1 核心技术栈
本方案基于三个核心技术组件协同实现 PDF 生成,各组件职责清晰、分工明确,共同完成从数据到 PDF 文档的转换流程:
|------------------------------|-------------------------|--------------------------------|
| 组件 | 作用 | 核心优势 |
| Flying Saucer(xhtmlrenderer) | 将 HTML/CSS 渲染为 PDF | 支持 CSS 2.1 标准,所见即所得,轻量高效 |
| OpenPDF | Flying Saucer 底层 PDF 实现 | 开源免费,支持中文、PDF加密、书签等高级功能 |
| Thymeleaf | 动态 HTML 模板渲染 | 与 Spring Boot 无缝集成,支持数据绑定、条件渲染 |
1.2 工作原理
核心流程:数据模型 → Thymeleaf 模板 → 动态 HTML → Flying Saucer 渲染 → PDF 文件
分步解析:
-
数据准备:Java 代码中构造需要展示的数据(Map、List、自定义对象)
-
模板渲染:Thymeleaf 将数据绑定到 HTML 模板,生成动态 HTML 字符串
-
PDF 转换:Flying Saucer 解析 HTML/CSS,渲染为 PDF 文档
-
输出:将 PDF 写入文件或 HTTP 响应流(支持下载)
二、核心方法详解
核心组件包括 SpringTemplateEngine(Thymeleaf 模板渲染核心)和 ITextRenderer(Flying Saucer PDF 渲染核心),其核心方法决定了模板渲染和 PDF 生成的核心逻辑。
2.1 SpringTemplateEngine 核心方法
SpringTemplateEngine 是 Thymeleaf 在 Spring Boot 环境下的核心实现类,负责将模板与数据模型结合生成 HTML 字符串,核心方法如下:
java
// 1. 核心渲染方法:将模板与上下文数据结合生成HTML
String process(String templateName, IContext context);
/* 参数说明:
- templateName:模板名称,默认查找classpath:/templates/目录下的.html文件
- context:上下文对象,存储渲染所需的变量,常用实现类为Context
*/
// 示例用法
Context context = new Context();
context.setVariable("data", reportData); // 注入数据
String html = templateEngine.process("report", context); // 渲染report.html模板
// 2. 清除模板缓存(开发环境常用)
void clearTemplateCache();
// 清除指定模板的缓存
void clearTemplateCacheFor(String templateName);
// 3. 检查模板是否缓存
boolean isTemplateCached(String templateName);
关键说明:Spring Boot 会自动装配 SpringTemplateEngine,无需手动创建,可直接通过 @Autowired 注入使用;开发环境建议关闭模板缓存(spring.thymeleaf.cache=false),避免修改模板后需重启项目。
2.2 ITextRenderer 核心方法
ITextRenderer 是 Flying Saucer 的核心类,负责将 HTML/CSS 渲染为 PDF,核心方法如下:
java
// 1. 初始化渲染器
ITextRenderer renderer = new ITextRenderer();
// 2. 加载中文字体(解决中文乱码核心方法)
ITextFontResolver fontResolver = renderer.getFontResolver();
fontResolver.addFont(String fontPath, String encoding, boolean embedded);
/* 参数说明:
- fontPath:字体文件路径(本地路径或classpath路径)
- encoding:编码格式,BaseFont.IDENTITY_H为Unicode编码,支持中文
- embedded:是否嵌入字体到PDF,true则PDF兼容性更好但体积更大,false则体积小需系统有对应字体
*/
// 3. 设置HTML内容(两种常用方式)
// 方式1:从HTML字符串加载
renderer.setDocumentFromString(String html);
// 方式2:从文件/URL加载
renderer.setDocument(File file);
renderer.setDocument(URL url);
// 4. 布局计算:解析HTML/CSS并计算元素位置
renderer.layout();
// 5. 生成PDF(两种常用方式)
// 方式1:写入输出流(响应下载常用)
renderer.createPDF(OutputStream os);
// 方式2:分页生成多PDF(需追加页面时使用)
renderer.createPDF(OutputStream os, boolean finish); // finish=false表示不结束文档
renderer.writeNextDocument(); // 追加下一页
renderer.finishPDF(); // 最终完成文档生成
// 6. 获取共享上下文,用于配置全局参数
SharedContext sharedContext = renderer.getSharedContext();
sharedContext.setDefaultFont("SimHei"); // 设置默认字体
sharedContext.setMarginTop(20); // 设置页面上边距
关键说明:ITextRenderer 的使用需遵循"初始化→配置字体→设置文档→布局→生成PDF"的流程;生成多页 PDF 时,需将 createPDF 的 finish 参数设为 false,追加完成后调用 finishPDF() 结束文档。
三、快速上手:项目搭建
掌握核心注解和方法后,即可进行项目搭建。本章节将从项目创建、目录结构、基础配置、启动类编写等方面,完整讲解项目搭建流程。
3.1 创建 Spring Boot 项目
推荐使用 Spring Initializr 快速创建,或手动编写 pom.xml。
核心依赖(pom.xml)
XML
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
</parent>
<dependencies>
<!-- Spring Web -->
<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>
<!-- Flying Saucer + OpenPDF -->
<dependency>
<groupId>org.xhtmlrenderer</groupId>
<artifactId>flying-saucer-pdf-openpdf</artifactId>
<version>9.1.22</version>
</dependency>
<!-- Lombok(简化代码,可选) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
3.2 项目目录结构
规范目录结构,便于项目维护和扩展:
html
pdf-generator/
├── src/main/java/com/example/pdf/
│ ├── PdfGeneratorApplication.java # 启动类
│ ├── controller/PdfController.java # 下载接口
│ ├── service/PdfService.java # 核心服务
│ ├── model/ReportData.java # 数据模型
│ └── utils/PdfUtils.java # 工具类
└── src/main/resources/
├── application.yml # 配置文件
├── templates/report.html # Thymeleaf模板
└── fonts/SimHei.ttf # 中文字体文件
3.3 基础配置(application.yml)
配置服务器端口、Thymeleaf 模板参数、PDF 字体相关参数:
XML
server:
port: 8080
spring:
thymeleaf:
cache: false # 开发环境关闭缓存,生产环境开启
mode: HTML
encoding: UTF-8
prefix: classpath:/templates/ # 模板存放路径
suffix: .html
# PDF相关配置 下文示例并未使用
pdf:
font:
path: classpath:fonts/SimHei.ttf # 字体路径
embedded: false # 是否嵌入字体(嵌入后PDF更大,兼容性更好)
3.4 启动类
编写 Spring Boot 应用入口类,用于启动应用:
java
package com.example.pdf;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class PdfGeneratorApplication {
public static void main(String[] args) {
SpringApplication.run(PdfGeneratorApplication.class, args);
}
}
四、核心实现:从模板到 PDF
项目搭建完成后,即可实现从数据模型定义、模板编写到 PDF 生成的完整核心逻辑。本章节将详细讲解数据模型、Thymeleaf 模板、PDF 工具类、控制器的编写。
4.1 数据模型(ReportData.java)
定义需要展示的数据结构,使用 Lombok 简化 getter/setter:
java
package com.example.pdf.model;
import lombok.Data;
import java.util.List;
@Data
public class ReportData {
private String title; // 报告标题
private String dateRange; // 日期范围
private List<UserOperation> operations; // 操作列表
}
// 子模型
@Data
public class UserOperation {
private String userAccount; // 用户账号
private String registrationDate;// 注册时间
private String totalOperations; // 总操作次数
private String operationType; // 操作类型
}
4.2 Thymeleaf 模板(report.html)
模板即预览,可直接在浏览器中调试样式,注意引入中文字体:
html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8"/>
<title>用户操作报告</title>
<style>
/* PDF页面设置:A4横向,边距20mm */
@page {
size: A4 landscape;
margin: 20mm;
}
/* 全局样式,指定中文字体 */
body {
font-family: "SimHei", "Microsoft YaHei", sans-serif;
font-size: 14px;
color: #333;
}
.title {
font-size: 24px;
font-weight: bold;
text-align: center;
margin-bottom: 20px;
}
.meta {
font-size: 14px;
margin-bottom: 15px;
color: #666;
}
/* 表格样式 */
table {
width: 100%;
border-collapse: collapse;
}
th, td {
border: 1px solid #ccc;
padding: 8px 10px;
text-align: center;
}
th {
background-color: #f5f5f5;
font-weight: bold;
}
</style>
</head>
<body>
<div class="title" th:text="${data.title}">用户高频操作报告</div>
<div class="meta">统计时间:<span th:text="${data.dateRange}">2025-12-01 至 2025-12-31</span></div>
<table>
<thead>
<tr>
<th>用户账号</th>
<th>注册时间</th>
<th>总操作次数</th>
<th>主要操作类型</th>
</tr>
</thead>
<tbody>
<!-- Thymeleaf循环渲染数据 -->
<tr th:each="op : ${data.operations}">
<td th:text="${op.userAccount}">admin001</td>
<td th:text="${op.registrationDate}">2025-11-20</td>
<td th:text="${op.totalOperations}">1000</td>
<td th:text="${op.operationType}">权限管理</td>
</tr>
</tbody>
</table>
</body>
</html>
4.3 PDF 工具类(PdfUtils.java)
封装 HTML 渲染和 PDF 生成逻辑,核心工具类,可直接复用:
java
package com.example.pdf.utils;
import com.lowagie.text.pdf.BaseFont;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.core.io.ClassPathResource;
import org.thymeleaf.context.Context;
import org.thymeleaf.spring6.SpringTemplateEngine;
import org.xhtmlrenderer.pdf.ITextRenderer;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Map;
public class PdfUtils {
/**
* 生成PDF并响应给前端(下载)
* @param templateName 模板名称
* @param data 渲染数据
* @param response 响应对象
* @param fileName 下载文件名
* @param templateEngine Thymeleaf引擎
*/
public static void generatePdfForDownload(String templateName, Map<String, Object> data,
HttpServletResponse response, String fileName,
SpringTemplateEngine templateEngine) throws Exception {
// 1. 渲染HTML(调用SpringTemplateEngine的process方法)
String html = renderHtml(templateName, data, templateEngine);
// 2. 响应配置
response.setContentType("application/pdf");
response.setHeader("Content-Disposition",
"attachment; filename=\"" + URLEncoder.encode(fileName, StandardCharsets.UTF_8) + ".pdf\"");
response.setHeader("Cache-Control", "no-cache, no-store");
// 3. 生成PDF并写入响应流(调用ITextRenderer的核心方法)
try (OutputStream outputStream = response.getOutputStream()) {
ITextRenderer renderer = new ITextRenderer();
// 加载中文字体(关键:解决中文乱码)
loadChineseFont(renderer);
// 设置HTML内容
renderer.setDocumentFromString(html);
// 布局计算
renderer.layout();
// 生成PDF
renderer.createPDF(outputStream);
}
}
/**
* 渲染Thymeleaf模板为HTML字符串
*/
private static String renderHtml(String templateName, Map<String, Object> data, SpringTemplateEngine templateEngine) {
Context context = new Context();
context.setVariables(data); // 注入数据
return templateEngine.process(templateName, context); // 核心渲染方法
}
/**
* 加载中文字体
*/
private static void loadChineseFont(ITextRenderer renderer) throws Exception {
// 从classpath加载字体文件
ClassPathResource fontResource = new ClassPathResource("fonts/SimHei.ttf");
String fontPath = fontResource.getURL().toString();
// 添加字体到渲染器(解决中文乱码核心方法)
renderer.getFontResolver().addFont(
fontPath,
BaseFont.IDENTITY_H, // Unicode编码(支持中文)
BaseFont.NOT_EMBEDDED // 不嵌入字体(减小PDF体积)
);
}
}
4.4 控制器(PdfController.java)
提供 HTTP 接口,供前端调用下载 PDF:
java
package com.example.pdf.controller;
import com.example.pdf.model.ReportData;
import com.example.pdf.model.UserOperation;
import com.example.pdf.utils.PdfUtils;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.thymeleaf.spring6.SpringTemplateEngine;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/pdf")
public class PdfController {
@Autowired
private SpringTemplateEngine templateEngine; // 注入模板引擎
/**
* 下载用户操作报告PDF
*/
@GetMapping("/download/report")
public void downloadReport(HttpServletResponse response) throws Exception {
// 1. 准备模拟数据(实际开发中从数据库查询)
Map<String, Object> data = new HashMap<>();
ReportData reportData = new ReportData();
reportData.setTitle("2025年12月用户高频操作报告");
reportData.setDateRange("2025-12-01 至 2025-12-31");
// 模拟操作数据
List<UserOperation> operations = new ArrayList<>();
operations.add(new UserOperation("admin001", "2025-11-20", "1200", "权限管理"));
operations.add(new UserOperation("user_002", "2025-11-25", "850", "数据查询"));
operations.add(new UserOperation("manager_003", "2025-12-01", "680", "报表导出"));
reportData.setOperations(operations);
data.put("data", reportData);
// 2. 生成PDF并下载
String fileName = "用户操作报告_" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
PdfUtils.generatePdfForDownload("report", data, response, fileName, templateEngine);
}
}
五、关键问题解决
在 PDF 生成开发过程中,常会遇到中文乱码、样式不生效、表格分页截断等问题。本章节将针对这些核心痛点,提供具体的解决方案。
5.1 中文乱码问题(核心重点)
Flying Saucer 默认不支持中文字体,必须手动加载,解决步骤:
- 下载中文字体文件(如 SimHei.ttf 黑体、msyh.ttc 微软雅黑),放入
resources/fonts目录

- 在工具类中通过
renderer.getFontResolver().addFont()加载字体

- 在 HTML 模板的 CSS 中指定字体:
font-family: "SimHei", sans-serif;

注意 :字体路径必须正确,可通过 ClassPathResource 确保跨环境兼容。
5.2 样式不生效问题
Flying Saucer 仅支持 CSS 2.1 标准,不支持 CSS3 特性(如 flex、grid、border-radius 等),解决方案:
-
使用基础 CSS 样式(float、position 替代 flex)
-
样式写在
<style>标签内(避免外部 CSS 文件) -
表格使用
border-collapse: collapse确保边框正常显示
5.3 表格分页截断问题
当表格内容过多跨页时,可能出现行截断,解决方案:
css
/* 避免表格行被分页截断 */
table {
page-break-inside: avoid;
}
tr {
page-break-inside: avoid;
}
/* 强制分页(如需) */
.page-break {
page-break-after: always;
}
六、进阶功能拓展
基于基础实现,可拓展多页 PDF 生成、图片嵌入、条件渲染等进阶功能,满足更复杂的业务需求。
6.1 嵌入图片到 PDF
支持本地图片、网络图片、Base64 图片,示例:
html
<!-- 本地图片(classpath下) -->
<img src="classpath:images/logo.png" alt="logo" width="100"/>
<!-- 网络图片 -->
<img src="https://example.com/logo.png" alt="logo" width="100"/>
<!-- Base64图片(适合动态生成的图片,如二维码) -->
<img th:src="'data:image/png;base64,' + ${qrCodeBase64}" alt="二维码"/>
6.2 多页 PDF 生成
如需生成多页 PDF(如多章节报告),可通过 writeNextDocument()追加页面:
java
// 多页PDF生成核心代码
ITextRenderer renderer = new ITextRenderer();
loadChineseFont(renderer);
// 第一页
String html1 = renderHtml("report-chapter1", data1, templateEngine);
renderer.setDocumentFromString(html1);
renderer.layout();
renderer.createPDF(outputStream, false); // finish=false,不结束文档
// 第二页
String html2 = renderHtml("report-chapter2", data2, templateEngine);
renderer.setDocumentFromString(html2);
renderer.layout();
renderer.writeNextDocument(); // 追加下一页
// 结束文档
renderer.finishPDF();
6.3 条件渲染与循环
利用 Thymeleaf 语法实现动态逻辑:
html
<!-- 条件渲染(根据状态显示不同内容) -->
<div th:if="${data.status == 'success'}" style="color: green;">
报告生成成功!
</div>
<div th:unless="${data.status == 'success'}" style="color: red;">
报告生成失败!
</div>
<!-- 循环渲染(带索引) -->
<tr th:each="op, stat : ${data.operations}">
<td th:text="${stat.index + 1}">1</td> <!-- 索引从0开始,+1转为1开始 -->
<td th:text="${op.userAccount}">admin001</td>
</tr>
七、性能优化建议
-
开启模板缓存:生产环境设置
spring.thymeleaf.cache=true,避免重复解析模板 -
字体优化:非特殊需求不嵌入字体(
BaseFont.NOT_EMBEDDED),减小 PDF 体积 -
异步生成:高并发场景下,使用
@Async异步生成 PDF,避免阻塞主线程 -
分批处理:大数据量报表(如10万条数据),建议分页生成后合并 PDF
八、示例仓库地址
附:参考资料
-
Flying Saucer 官方文档:https://flyingsaucerproject.github.io/flyingsaucer/
-
Thymeleaf 官方文档:https://www.thymeleaf.org/documentation.html
-
OpenPDF 官方仓库:https://github.com/LibrePDF/OpenPDF