java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > SpringBoot生成PDF

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

作者:风清已存在

在实际开发中,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 文件

分步解析:

二、核心方法详解

核心组件包括 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 等),解决方案:

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>

七、性能优化建议

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

您可能感兴趣的文章:
阅读全文