导读:本期聚焦于俊华创作的《Spring Boot 中如何高效生成 PDF 并优化打印样式?》,敬请观看详情。直接调用 PDF 底层 API 绘制表格和文本,调整一次版式往往要改动大量坐标参数;而先把页面写成 HTML 再转换成 PDF,能沿用 CSS 的排版思路,分页、字体、边距都更直观。Spring Boot 项目里常用 Thymeleaf 渲染模板、Flying Saucer 或 OpenHTMLToPDF 完成转换,这套方案的关键在于解决中文字体嵌入、CSS 打印规则适配以及分页控制。本文会从技术选型、字体注册、@page 与分页属性、响应头输出等几个方面展开,并给出可直接运行的 Java 和 CSS 示例,帮助开发者在合同、单据、报表等场景中快速生成版式稳定、打印效果可控的 PDF 文件。

Spring Boot 里生成 PDF 的场景很常见,比如电子合同、发货单、对账单、考试报告等。方案大致分为两类:一类是以 iText、PDFBox 为代表的底层绘制,另一类是把 HTML 页面转换成 PDF。底层绘制能精确控制每个坐标,但遇到复杂表格、长文本换行、动态内容时开发和维护成本会明显上升。HTML 转 PDF 则可以直接复用前端样式经验,尤其适合需要频繁调整打印效果的场景。

Spring Boot 中如何高效生成 PDF 并优化打印样式?

一、技术选型:HTML 转 PDF 的组合思路

在 JVM 生态里,HTML 转 PDF 的常见组合是模板引擎加渲染器。模板层可以使用 Thymeleaf、Freemarker 或 Velocity,渲染层则有 Flying Saucer、OpenHTMLToPDF 等。Flying Saucer 基于 iText 2.x 实现,对 CSS 2.1 支持较完整,能够识别 @page 规则、分页属性以及部分打印媒体样式。OpenHTMLToPDF 是 Flying Saucer 的延续分支,修复了不少旧问题,对 CSS 3 的某些特性支持更好。

从实际项目角度看,Thymeleaf 与 Flying Saucer 的搭配资料较多,集成也简单。但如果项目对现代 CSS 特性依赖较强,例如需要使用 flex 或 grid 排版,建议优先评估 OpenHTMLToPDF。不过要注意,PDF 的排版模型与浏览器并不完全一致,即便使用支持度较好的渲染器,仍然建议以表格、块级元素、浮动等传统手段为主,打印样式才能更稳定。

在 Spring Boot 工程中引入依赖后,核心流程通常是:先读取模板文件,再注入动态数据生成完整 HTML 字符串,最后通过渲染器的 API 输出 PDF 字节数组。下面这段 Maven 配置展示了 OpenHTMLToPDF 的引入方式,版本号可以根据实际仓库调整。

<dependency>
    <groupId>com.openhtmltopdf</groupId>
    <artifactId>openhtmltopdf-pdfbox</artifactId>
    <version>1.0.10</version>
</dependency>

如果团队更熟悉 Flying Saucer,也可以使用 org.xhtmlrenderer 下的依赖,但需要注意中文字体必须单独注册,否则生成出来的 PDF 中文会变成乱码或空白。

二、中文字体嵌入与基础打印样式

中文字体是 HTML 转 PDF 绕不开的问题。渲染器不会自动识别操作系统字体,必须把字体文件当作资源加载并注册到渲染上下文。常用的中文字体包括宋体、黑体、微软雅黑等,如果涉及商用发布,建议使用开源字体如思源宋体、思源黑体或阿里巴巴普惠体。字体文件可以放在 Spring Boot 的 resources/fonts 目录下,服务启动后读取为流。

注册字体时,需要把字体文件添加到渲染器的字体解析器,并在 CSS 中通过 font-family 指定对应的字体名称。这里的字体名称要与注册时传入的名称保持一致,而不是文件名的简单去后缀。下面是一段注册思路的 Java 代码,展示了如何加载 resources/fonts/simsun.ttf 并生成 PDF。

import java.io.InputStream;
import java.io.OutputStream;
import com.openhtmltopdf.pdfboxout.PdfRendererBuilder;

public byte[] generatePdf(String htmlContent) throws Exception {
    try (InputStream fontStream = getClass().getResourceAsStream("/fonts/simsun.ttf")) {
        PdfRendererBuilder builder = new PdfRendererBuilder();
        builder.useFont(fontStream, "SimSun");
        builder.withHtmlContent(htmlContent, null);
        try (OutputStream os = new java.io.ByteArrayOutputStream()) {
            builder.toStream(os);
            builder.run();
            return ((java.io.ByteArrayOutputStream) os).toByteArray();
        }
    }
}

CSS 的基础打印样式同样重要。页面尺寸、边距、字体大小都可以通过 @page 和常规样式控制。以下 CSS 片段定义了 A4 页面、合理的页边距以及全局中文字体,写在 HTML 模板的 <style> 标签中即可。

@page {
    size: A4;
    margin: 2cm 1.8cm 2.2cm 1.8cm;
}
body {
    font-family: "SimSun", serif;
    font-size: 12pt;
    line-height: 1.6;
    color: #333;
}
table {
    width: 100%;
    border-collapse: collapse;
}
td, th {
    border: 1px solid #999;
    padding: 6pt 8pt;
}

有时候生成出来的 PDF 字体偏粗或行距过密,往往是因为没有给字体指定正确的字重,或者浏览器预览与 PDF 渲染器对默认行高的计算不同。建议在 body 上显式设置 line-height,并避免使用 em 单位做精细排版,pt 和 cm 更适合打印场景。

三、分页控制与页眉页脚

打印样式的核心痛点之一就是分页。报表表格动辄几十行,如果不做控制,表格行很可能被拦腰截断,或者标题与内容分离。CSS 的 page-break-before、page-break-after 以及 break-inside 属性可以解决大部分问题。Flying Saucer 和 OpenHTMLToPDF 都支持这些规则的常用取值,尤其是 break-inside: avoid 可以阻止某个块元素在分页时被切断。

对于表格,推荐将表头放在 <thead> 中,并给 <tr>、<td> 设置 break-inside: avoid。这样每一页都会自动重复表头,单行内容也不会被切开。下面的 CSS 示例适用于合同明细、发货清单等长表格。

thead {
    display: table-header-group;
}
tr {
    break-inside: avoid;
    page-break-inside: avoid;
}
.section {
    page-break-before: always;
}
.avoid-break {
    page-break-inside: avoid;
}

页眉页脚与页码可以使用 @page 的边距框规则来实现。例如 @bottom-center 可以定义页面底部中央的内容,使用 content: counter(page) 可以输出当前页码。这种方式不依赖额外 Java 代码,调整起来非常直观。不过需要注意,Flying Saucer 旧版本对 @bottom-center 的支持因版本而异,OpenHTMLToPDF 的支持更稳定一些。

@page {
    @bottom-center {
        content: "第 " counter(page) " 页 / 共 " counter(pages) " 页";
        font-size: 9pt;
        color: #666;
    }
    @top-right {
        content: "发货单号:D20240201";
        font-size: 9pt;
        color: #666;
    }
}

如果希望在文档第一页不显示页眉,可以给第一页单独定义 @page:first 并留空对应边距框。若是合同封面、报告首页等场景,这个技巧非常实用。

四、Spring Boot 集成与容易忽略的细节

在 Spring Boot 控制器中,生成 PDF 后一般直接通过 HttpServletResponse 输出,让浏览器以附件或内联方式打开。响应头中的 Content-Type 应设置为 application/pdf,Content-Disposition 可以控制文件名和打开方式。以下是一个完整的控制器示例,模板使用 Thymeleaf 渲染,再调用 PDF 生成服务。

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;

import javax.servlet.http.HttpServletResponse;
import java.io.OutputStream;

@Controller
public class PdfController {

    private final TemplateEngine templateEngine;
    private final PdfService pdfService;

    public PdfController(TemplateEngine templateEngine, PdfService pdfService) {
        this.templateEngine = templateEngine;
        this.pdfService = pdfService;
    }

    @GetMapping("/export/invoice")
    public void exportInvoice(HttpServletResponse response) throws Exception {
        Context context = new Context();
        context.setVariable("orderNo", "D20240201");
        context.setVariable("customerName", "某科技有限公司");
        String html = templateEngine.process("invoice", context);

        byte[] pdfBytes = pdfService.generatePdf(html);

        response.setContentType("application/pdf");
        response.setHeader("Content-Disposition", "attachment; filename=invoice.pdf");
        response.setContentLength(pdfBytes.length);

        try (OutputStream os = response.getOutputStream()) {
            os.write(pdfBytes);
            os.flush();
        }
    }
}

图片路径是另一个容易踩坑的地方。渲染器通常无法直接识别相对路径,应该使用绝对文件路径或完整的 URL。例如模板中的 <img> 标签最好写成 <img src="file:/data/images/logo.png"/> 或者 <img src="https://your-domain/logo.png"/>。如果图片只放在 classpath 下,需要先把资源复制到临时目录,再拼接 file: 路径。

性能方面,频繁创建字体流和渲染器会带来不必要的开销。字体流可以缓存在内存中,渲染器也可以在每次调用时新创建,但字体解析器可以复用。对于高并发场景,建议将 HTML 模板编译结果缓存起来,只替换动态数据,减少模板解析时间。此外,长文档的生成耗时与页数基本成正比,必要时可以拆分成多个小 PDF 再合并。

最后要提醒的是,CSS 中的 flexbox、grid、position: absolute 等现代布局在 PDF 渲染器中支持度有限。调试打印样式时,不要用浏览器渲染结果作为最终依据,最好直接查看生成的 PDF。可以先把 HTML 模板保存成独立文件,用渲染器的调试模式检查警告信息,快速定位不支持的 CSS 规则。

Spring BootPDF生成打印样式优化修改时间:2026-09-29 17:10:12

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/0929/63477.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。