SpringBoot+Flying Saucer+Thymeleaf实现PDF生成的完整指南

 更新时间:2026年01月07日 09:07:33   作者:风清已存在  
在实际开发中,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 标准,所见即所得,轻量高效
OpenPDFFlying 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 字符串,核心方法如下:

// 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,核心方法如下:

// 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)

<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 项目目录结构

规范目录结构,便于项目维护和扩展:

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 字体相关参数:

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 应用入口类,用于启动应用:

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:

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)

模板即预览,可直接在浏览器中调试样式,注意引入中文字体:

<!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 生成逻辑,核心工具类,可直接复用:

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:

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 表格分页截断问题

当表格内容过多跨页时,可能出现行截断,解决方案:

/* 避免表格行被分页截断 */
table {
    page-break-inside: avoid;
}
tr {
    page-break-inside: avoid;
}

/* 强制分页(如需) */
.page-break {
    page-break-after: always;
}

六、进阶功能拓展

基于基础实现,可拓展多页 PDF 生成、图片嵌入、条件渲染等进阶功能,满足更复杂的业务需求。

6.1 嵌入图片到 PDF

支持本地图片、网络图片、Base64 图片,示例:

<!-- 本地图片(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()追加页面:

// 多页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 语法实现动态逻辑:

<!-- 条件渲染(根据状态显示不同内容) -->
<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

以上就是SpringBoot+Flying Saucer+Thymeleaf实现PDF生成的完整指南的详细内容,更多关于SpringBoot生成PDF的资料请关注脚本之家其它相关文章!

相关文章

  • java调用Restful接口的三种方法

    java调用Restful接口的三种方法

    本文主要介绍了java调用Restful接口的三种方法,主要包括HttpURLConnection实现,HttpClient实现和Spring的RestTemplate,具有一定的参考,感兴趣的可以了解一下    
    2021-08-08
  • Java多线程并发生产者消费者设计模式实例解析

    Java多线程并发生产者消费者设计模式实例解析

    这篇文章主要介绍了Java多线程并发生产者消费者设计模式实例解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-03-03
  • java从字符串中提取数字的简单实例

    java从字符串中提取数字的简单实例

    下面小编就为大家带来一篇java从字符串中提取数字的简单实例。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2016-10-10
  • Spring Cloud Feign实现动态URL

    Spring Cloud Feign实现动态URL

    本文主要介绍了Spring Cloud Feign实现动态URL,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-02-02
  • 自定义mybatis插件如何实现sql日志打印

    自定义mybatis插件如何实现sql日志打印

    这篇文章主要介绍了自定义mybatis插件如何实现sql日志打印问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-05-05
  • Java向数据库中插入数据后获取自增ID的常用方法

    Java向数据库中插入数据后获取自增ID的常用方法

    有时候因为新增的需求需要获取刚刚新增的数据的自增的主键ID,下面这篇文章主要给大家介绍了关于Java向数据库中插入数据后获取自增ID的常用方法,文中通过代码介绍的非常详细,需要的朋友可以参考下
    2023-11-11
  • idea打开和读取*properties文件乱码的解决

    idea打开和读取*properties文件乱码的解决

    本文主要介绍了idea打开和读取*properties文件乱码的解决,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-09-09
  • 使用Nacos作为配置中心的命名空间、配置分组

    使用Nacos作为配置中心的命名空间、配置分组

    文章详细介绍了Spring Cloud Config配置中心的命名空间、配置集、配置集ID、配置分组以及如何在微服务中加载和使用这些配置,通过配置中心,可以实现配置隔离和集中管理,简化微服务的配置维护
    2024-12-12
  • java二叉树的非递归遍历

    java二叉树的非递归遍历

    二叉树的递归遍历比较简单,这里就不聊了,今天主要聊聊二叉树的非递归遍历,主要借助于“栈”后进先出的特性来保存节点的顺序,先序遍历和中序遍历相对来说比较简单,重点理解后序遍历
    2020-12-12
  • java fastjson传输long数据却接收到了int的问题

    java fastjson传输long数据却接收到了int的问题

    这篇文章主要介绍了java fastjson传输long数据却接收到了int的问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-01-01

最新评论