Spring Boot + Flying Saucer + Thymeleaf PDF 完整指南

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 文件

分步解析:

  1. 数据准备:Java 代码中构造需要展示的数据(Map、List、自定义对象)

  2. 模板渲染:Thymeleaf 将数据绑定到 HTML 模板,生成动态 HTML 字符串

  3. PDF 转换:Flying Saucer 解析 HTML/CSS,渲染为 PDF 文档

  4. 输出:将 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>

七、性能优化建议

  1. 开启模板缓存:生产环境设置 spring.thymeleaf.cache=true,避免重复解析模板

  2. 字体优化:非特殊需求不嵌入字体(BaseFont.NOT_EMBEDDED),减小 PDF 体积

  3. 异步生成:高并发场景下,使用 @Async 异步生成 PDF,避免阻塞主线程

  4. 分批处理:大数据量报表(如10万条数据),建议分页生成后合并 PDF

八、示例仓库地址

https://gitcode.com/qq_49474843/springboot-pdf-report.githttps://gitcode.com/qq_49474843/springboot-pdf-report.git

附:参考资料

相关推荐
乌日尼乐5 小时前
【Java基础整理】Java字符串处理,String、StringBuffer、StringBuilder
java·后端
全栈独立开发者5 小时前
点餐系统装上了“DeepSeek大脑”:基于 Spring AI + PgVector 的 RAG 落地指南
java·人工智能·spring
dmonstererer5 小时前
【k8s设置污点/容忍】
java·容器·kubernetes
super_lzb5 小时前
mybatis拦截器ParameterHandler详解
java·数据库·spring boot·spring·mybatis
程序之巅5 小时前
VS code 远程python代码debug
android·java·python
我是Superman丶5 小时前
【异常】Spring Ai Alibaba 流式输出卡住无响应的问题
java·后端·spring
墨雨晨曦885 小时前
Nacos
java
invicinble5 小时前
seata的认识与实际开发要做的事情
java
乌日尼乐6 小时前
【Java基础整理】Java多线程
java·后端
2501_941870566 小时前
从配置频繁变动到动态配置体系落地的互联网系统工程实践随笔与多语言语法思考
java·前端·python