← 返回

PDF 模板生成实战:iText 5.x/9.x 方案对比与开源替代

📅 2026-05-15 | 🏷️ Java, PDF, iText, OpenPDF, PDFBox | 📖 阅读约 15 分钟

一、背景与问题

在企业级应用中,大量业务文档需要模板化生成:保单、合同、告知书、发票、审批单……这些文档有三个共同特征:

  1. 格式固定 — 排版、字体、Logo 位置都是确定的
  2. 内容动态 — 姓名、金额、日期等字段每次不同
  3. 批量生产 — 高峰期可能一次生成数千份

常见做法是:先用 Word 制作模板 → 用 Java 填充数据 → 输出 PDF

本文对比两种主流方案,给出完整代码和踩坑记录。

A4 页面尺寸公式

PDF 中常用的 A4 页面尺寸为 210mm × 297mm。在 iText 中使用 point(磅)为单位,换算关系:1 inch = 72 pt = 25.4 mm。

因此 A4 的磅值为:宽 595.28 pt,高 841.89 pt。

字体大小的 pt 与 px 换算(以 96 DPI 屏幕为基准):px = pt × 4/3。

即 12pt ≈ 16px。


二、方案分析

2.1 主流 PDF 库对比

特性iText 5.xiText 9.xPDFBox 3.0OpenPDF 3.0
当前公开版本5.5.13.49.6.03.0.7 (2026-03)3.0.4
许可证AGPL v3(5.5.13+)AGPL v3Apache 2.0LGPL / MPL
商用免费⚠️ 需开源或购买授权⚠️ 需开源或购买授权✅ 完全免费✅ 完全免费
表单填充✅ 优秀✅ 优秀✅ 支持✅ 基于 iText 5.x fork
中文支持✅ 需嵌入字体✅ 需嵌入字体✅ 需嵌入字体✅ 需嵌入字体
PDF 2.0✅ 原生支持
社区活跃度低(维护模式)高(官方主推)高(Apache 顶级项目)中(社区维护)
API 易用性⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
学习资源丰富丰富中等较少

2.2 许可证问题详解

iText AGPL 许可证风险

iText 的 AGPL v3 意味着什么?

  • 如果你的软件以 AGPL 发布,可以免费使用
  • 如果是闭源商业软件,必须购买商业授权(iText 商业版按年收费,价格不菲)
  • 所谓"内网规避"在法律上有争议,不建议依赖,AGPL 的触发条件是"通过网络与用户交互",企业内部系统也可能被认定为触发场景
  • 即使是纯内部使用,如果代码分发给第三方(如外包、子公司),也需要合规
  • 法律建议:如果你的项目不是 AGPL 开源的,请直接选择 PDFBox 或 OpenPDF

开源替代方案详解:

方案许可证特点推荐场景
Apache PDFBox 3.0Apache 2.0更安全的许可证,Apache 顶级项目,功能全面商业项目首选,特别是表单填充、文本提取
OpenPDF 3.0LGPL/MPLiText 4.x 的社区 fork,API 与 iText 5.x 高度兼容从 iText 5.x 迁移成本更低
Flying Saucer (OpenPDF-html)LGPLHTML → PDF 转换需要从 HTML 生成 PDF 的场景
JasperReportsLGPL报表引擎,支持多种输出格式复杂报表场景

选型建议:

场景推荐方案
开源项目iText 9.x(功能高强度)
商业项目、预算充足iText 9.x 商业版
商业项目、零预算PDFBox 3.0(Apache 2.0,更安全)
从 iText 5.x 迁移OpenPDF 3.0(API 兼容)
只需要简单表单填充PDFBox(Apache 许可,更安全)
HTML → PDFFlying Saucer + OpenPDF

三、方案一:iText 5.x 实现

3.1 依赖配置

<!-- pom.xml -->
<dependencies>
    <!-- iText 5.x(注意许可证) -->
    <dependency>
        <groupId>com.itextpdf</groupId>
        <artifactId>itextpdf</artifactId>
        <version>5.5.13.4</version>
    </dependency>
    <!-- iTextAsian 中文字体支持(如需) -->
    <dependency>
        <groupId>com.itextpdf</groupId>
        <artifactId>itext-asian</artifactId>
        <version>5.2.0</version>
    </dependency>
</dependencies>

3.2 Word 模板制作

制作流程:

  1. 在 Word 中设计文档排版
  2. 在需要动态填充的位置插入 表单域(开发工具 → 表单域)
  3. 给每个表单域命名(如 userNamepolicyNo
  4. 另存为 PDF,表单域会自动转为 PDF AcroField

模板设计规范:

  • 表单域命名使用英文驼峰,避免中文和特殊字符
  • 同名域会自动同步填充(适用于多页签名场景)
  • Checkbox 使用"复选框型表单域",勾选值设为 Yes

3.3 PDFUtil 工具类完整代码

import com.itextpdf.text.DocumentException;
import com.itextpdf.text.pdf.AcroFields;
import com.itextpdf.text.pdf.BaseFont;
import com.itextpdf.text.pdf.PdfReader;
import com.itextpdf.text.pdf.PdfStamper;

import java.io.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * PDF 表单填充工具类(基于 iText 5.x)
 *
 * 功能:读取 PDF 模板 → 填充表单域 → 输出新 PDF
 */
public class PdfUtil {

    /**
     * 默认中文字体路径(根据部署环境调整)
     * 推荐将字体文件放在 resources/fonts/ 下随项目打包
     */
    private static final String FONT_PATH = "/fonts/simsun.ttc";

    /**
     * 填充 PDF 模板并输出到指定路径
     *
     * @param templatePath  PDF 模板文件路径
     * @param outputPath    输出 PDF 路径
     * @param fieldData     表单域数据(key = 域名, value = 填充值)
     * @throws Exception    文件读写或文档处理异常
     */
    public static void fillPdf(String templatePath,
                                String outputPath,
                                Map<String, String> fieldData) throws Exception {
        // 读取模板
        PdfReader reader = new PdfReader(templatePath);
        // 创建 Stamper(true = 追加模式,false = 覆盖)
        PdfStamper stamper = new PdfStamper(reader, new FileOutputStream(outputPath));

        try {
            // 获取表单域
            AcroFields form = stamper.getAcroFields();

            // 嵌入中文字体(关键!不嵌入中文会显示为方块)
            BaseFont bfChinese = BaseFont.createFont(
                    FONT_PATH + ",0",          // ",0" 指定 TTC 字体集合中的第 0 个字体
                    BaseFont.IDENTITY_H,       // 水平书写
                    BaseFont.EMBEDDED           // 嵌入字体到 PDF
            );
            form.addSubstitutionFont(bfChinese);

            // 遍历填充数据
            for (Map.Entry<String, String> entry : fieldData.entrySet()) {
                String fieldName = entry.getKey();
                String fieldValue = entry.getValue();

                if (fieldValue == null) {
                    fieldValue = "";
                }

                // 判断字段类型
                int fieldType = form.getFieldType(fieldName);
                if (fieldType == AcroFields.FIELD_TYPE_CHECKBOX) {
                    // Checkbox 处理:值为 "Yes" 时勾选
                    form.setField(fieldName, "Yes");
                    // 如果需要取消勾选,传空字符串
                    // form.setField(fieldName, "");
                } else {
                    // 普通文本域
                    form.setField(fieldName, fieldValue);
                }
            }

            // 关键:设置为不可编辑(flat),表单域变为静态内容
            stamper.setFormFlattening(true);

        } finally {
            stamper.close();
            reader.close();
        }
    }

    /**
     * 批量生成 PDF
     *
     * @param templatePath  模板路径
     * @param outputDir     输出目录
     * @param dataList      批量数据列表,每条数据对应一份 PDF
     * @param nameField     用于生成文件名的字段名(如 "policyNo")
     * @throws Exception    处理异常
     */
    public static void batchFill(String templatePath,
                                  String outputDir,
                                  List<Map<String, String>> dataList,
                                  String nameField) throws Exception {
        File dir = new File(outputDir);
        if (!dir.exists()) {
            dir.mkdirs();
        }

        for (int i = 0; i < dataList.size(); i++) {
            Map<String, String> data = dataList.get(i);
            // 用指定字段值或序号命名
            String fileName = data.getOrDefault(nameField, String.valueOf(i + 1));
            String outputPath = outputDir + File.separator + fileName + ".pdf";
            fillPdf(templatePath, outputPath, data);
        }
    }
}

3.4 中文字体处理(TTC 字体加载)

这是容易踩坑的地方,单独展开说明:

// ❌ 错误写法:直接用路径,TTC 文件会报错
BaseFont bf = BaseFont.createFont("/fonts/simsun.ttc", BaseFont.IDENTITY_H, BaseFont.EMBEDDED);

// ✅ 正确写法:TTC 文件必须指定字体索引
// ",0" 表示 TTC 集合中的第 0 个字体(SimSun 宋体)
// ",1" 表示第 1 个字体(如 SimSun 粗体)
BaseFont bf = BaseFont.createFont("/fonts/simsun.ttc,0", BaseFont.IDENTITY_H, BaseFont.EMBEDDED);

// ✅ 也可以从 classpath 加载
InputStream is = PdfUtil.class.getResourceAsStream("/fonts/simsun.ttc");
byte[] fontBytes = is.readAllBytes();
BaseFont bf = BaseFont.createFont("simsun.ttc,0", BaseFont.IDENTITY_H, BaseFont.EMBEDDED, null, fontBytes, null);

常用中文字体清单:

字体文件字体名索引用途
simsun.ttc宋体0正文(推荐)
simhei.ttf黑体标题
msyh.ttc微软雅黑0屏幕显示优化
字体嵌入注意事项
  1. 版权问题:微软雅黑、宋体等字体商用需要授权。思源黑体(Source Han Sans)和思源宋体(Source Han Serif)是免费商用的替代方案。
  2. 文件大小:嵌入中文字体后 PDF 体积会显著增大(每个字体约 5-15MB),可通过子集化(subset)减小体积。
  3. TTC 与 TTF:TTC(TrueType Collection)是多个字体的集合,必须指定索引;TTF 是单个字体,无需索引。

3.5 Checkbox 显示 × 的坑与解决

问题描述: 使用 iText 5.x 填充 Checkbox 后,勾选状态显示为 “×” 而非 “✓"。

原因: PDF 表单域的 Checkbox 有不同的外观状态(check style),默认可能是 cross

解决方案:

/**
 * 设置 Checkbox 勾选样式
 *
 * @param form       AcroFields 对象
 * @param fieldName  Checkbox 域名
 * @param checked    是否勾选
 */
public static void setCheckbox(AcroFields form, String fieldName, boolean checked) {
    if (checked) {
        form.setField(fieldName, "Yes");
        // 尝试切换外观为 check(✓)
        // 注意:这取决于模板中定义的外观名称
        // 如果模板是用 Word 转的 PDF,外观名称可能是 "Yes", "1", "On" 等
        // 需要逐一尝试
        String[] states = form.getAppearanceStates(fieldName);
        System.out.println("可用状态: " + java.util.Arrays.toString(states));
    } else {
        // 取消勾选:传空或 Off
        form.setField(fieldName, "Off");
    }
}

末尾推荐做法: 在 Word 模板中将 Checkbox 的选中符号设为 “✓"(U+2713),再转为 PDF,这样填充时直接设为 “Yes” 就能显示 ✓。

3.6 使用示例

import java.util.HashMap;
import java.util.Map;

public class PdfDemo {
    public static void main(String[] args) throws Exception {
        // 准备数据
        Map<String, String> data = new HashMap<>();
        data.put("userName", "张三");
        data.put("policyNo", "POL-2024-001234");
        data.put("effectiveDate", "2024年01月01日");
        data.put("expiryDate", "2025年01月01日");
        data.put("premium", "¥12,800.00");
        data.put("isAgree", "Yes");  // Checkbox

        // 填充 PDF
        PdfUtil.fillPdf(
                "/templates/policy_template.pdf",  // 模板路径
                "/output/policy_001.pdf",           // 输出路径
                data
        );

        System.out.println("PDF 生成完成!");
    }
}

四、方案二:iText 9.x 实现

4.1 依赖配置

<dependencies>
    <!-- iText 9 核心 -->
    <dependency>
        <groupId>com.itextpdf</groupId>
        <artifactId>itext7-core</artifactId>
        <version>9.6.0</version>
        <type>pom</type>
    </dependency>
    <!-- 中文字体支持 -->
    <dependency>
        <groupId>com.itextpdf</groupId>
        <artifactId>font-asian</artifactId>
        <version>9.6.0</version>
    </dependency>
</dependencies>

4.2 代码示例

import com.itextpdf.forms.PdfAcroForm;
import com.itextpdf.forms.fields.PdfFormField;
import com.itextpdf.kernel.font.PdfFont;
import com.itextpdf.kernel.font.PdfFontFactory;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.kernel.pdf.PdfWriter;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

/**
 * PDF 表单填充工具类(基于 iText 9.x)
 *
 * iText 9.x 的 API 更现代,采用 Builder 模式
 */
public class PdfUtilV7 {

    private static final String FONT_PATH = "/fonts/simsun.ttc";

    /**
     * 填充 PDF 模板
     *
     * @param templatePath 模板路径
     * @param outputPath   输出路径
     * @param fieldData    表单数据
     * @throws IOException IO 异常
     */
    public static void fillPdf(String templatePath,
                                String outputPath,
                                Map<String, String> fieldData) throws IOException {
        // iText 9 使用 PdfDocument 统一管理
        PdfDocument pdfDoc = new PdfDocument(
                new PdfReader(templatePath),
                new PdfWriter(outputPath)
        );

        try {
            // 获取表单
            PdfAcroForm form = PdfAcroForm.getAcroForm(pdfDoc, true);

            // 加载中文字体
            PdfFont chineseFont = PdfFontFactory.createFont(
                    FONT_PATH + ",0",
                    "Identity-H"    // 编码方式
            );

            // 填充字段
            Map<String, PdfFormField> fields = form.getAllFormFields();
            for (Map.Entry<String, String> entry : fieldData.entrySet()) {
                String fieldName = entry.getKey();
                String fieldValue = entry.getValue();

                PdfFormField field = fields.get(fieldName);
                if (field == null) {
                    System.err.println("字段不存在: " + fieldName);
                    continue;
                }

                // 设置值和字体
                field.setValue(fieldValue != null ? fieldValue : "");
                field.setFont(chineseFont);
                field.setFontSize(10);  // 根据模板调整
            }

            // 扁平化(不可编辑)
            form.flattenFields();

        } finally {
            pdfDoc.close();
        }
    }
}

4.3 iText 5.x vs 9.x API 差异对照

功能iText 5.xiText 9.x
核心类PdfReader + PdfStamperPdfDocument
表单操作AcroFieldsPdfAcroForm
字体加载BaseFont.createFont()PdfFontFactory.createFont()
扁平化stamper.setFormFlattening(true)form.flattenFields()
关闭资源需手动 close reader + stamperpdfDoc.close() 即可
遍历字段form.getFields() 返回 Map<String, AcroFields.Item>form.getAllFormFields() 返回 Map<String, PdfFormField>

4.4 iText 7.x → 9.x 升级说明

iText 从 7.x 升级到 9.x 是一次大版本升级,主要变化:

变化项说明
包名不变仍为 com.itextpdf,API 整体兼容
更低 JDKJDK 11+(7.x 支持 JDK 8)
PDF 2.0 增强原生支持 PDF 2.0、PDF/UA-2、PDF/A-4
WTPDF 支持新增 WellTaggedPdfDocument
颜色对比度检查PDF/UA 文档创建时自动检测前景/背景色对比度
数字签名增强扩展信任列表验证(含非欧盟国家)
模块化拆分为更细粒度的模块(kernel、forms、layout 等)

Maven 坐标变化:

<!-- iText 7.x -->
<artifactId>itext7-core</artifactId>
<version>7.2.6</version>

<!-- iText 9.x(坐标不变,版本号更新) -->
<artifactId>itext7-core</artifactId>
<version>9.6.0</version>
升级注意事项
  1. iText 9.x 要求 JDK 11+,JDK 8 项目无法使用
  2. 部分内部 API 有变化,但表单填充(PdfAcroForm)、字体加载(PdfFontFactory)等公开 API 基本兼容
  3. 建议升级后重新测试中文显示和表单填充功能
  4. 如果项目无法升级到 JDK 11,继续使用 iText 7.x 末尾的 7.2.x 版本

五、方案三:PDFBox 3.0 实现(开源替代)

为什么推荐 PDFBox?
PDFBox 3.0 是 Apache 顶级项目,采用 Apache 2.0 许可证,商业项目完全免费,无任何法律风险。功能覆盖表单填充、文本提取、PDF 合并拆分、数字签名等,足以满足大多数企业需求。

5.1 依赖配置

<dependencies>
    <!-- Apache PDFBox 3.0 -->
    <dependency>
        <groupId>org.apache.pdfbox</groupId>
        <artifactId>pdfbox</artifactId>
        <version>3.0.7</version>
    </dependency>
</dependencies>

5.2 PDFBox 表单填充代码

import org.apache.pdfbox.Loader;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDResources;
import org.apache.pdfbox.pdmodel.font.PDType0Font;
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
import org.apache.pdfbox.pdmodel.interactive.form.PDField;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;

/**
 * PDF 表单填充工具类(基于 Apache PDFBox 3.0)
 *
 * 许可证:Apache 2.0,商业项目完全免费
 */
public class PdfBoxUtil {

    private static final String FONT_PATH = "/fonts/simsun.ttc";

    /**
     * 填充 PDF 模板
     *
     * @param templatePath 模板路径
     * @param outputPath   输出路径
     * @param fieldData    表单数据
     * @throws IOException IO 异常
     */
    public static void fillPdf(String templatePath,
                                String outputPath,
                                Map<String, String> fieldData) throws IOException {
        // PDFBox 3.0 使用 Loader 加载文档
        try (PDDocument document = Loader.loadPDF(new File(templatePath))) {
            PDAcroForm acroForm = document.getDocumentCatalog().getAcroForm();

            if (acroForm == null) {
                throw new IllegalArgumentException("PDF 没有表单域");
            }

            // 加载中文字体,并设置为表单默认外观字体
            PDResources resources = acroForm.getDefaultResources();
            if (resources == null) {
                resources = new PDResources();
                acroForm.setDefaultResources(resources);
            }
            try (InputStream fontStream = PdfBoxUtil.class.getResourceAsStream(FONT_PATH)) {
                if (fontStream == null) {
                    throw new FileNotFoundException("字体文件不存在: " + FONT_PATH);
                }
                PDType0Font chineseFont = PDType0Font.load(document, fontStream);
                COSName fontName = resources.add(chineseFont);
                acroForm.setDefaultAppearance("/" + fontName.getName() + " 10 Tf 0 g");
            }

            // 填充字段
            for (Map.Entry<String, String> entry : fieldData.entrySet()) {
                String fieldName = entry.getKey();
                String fieldValue = entry.getValue();

                PDField field = acroForm.getField(fieldName);
                if (field == null) {
                    System.err.println("字段不存在: " + fieldName);
                    continue;
                }

                // 设置字段值
                field.setValue(fieldValue != null ? fieldValue : "");
            }

            // 扁平化表单(不可编辑)
            acroForm.flatten();

            // 保存
            document.save(new File(outputPath));
        }
    }

    /**
     * 批量生成 PDF
     */
    public static void batchFill(String templatePath,
                                  String outputDir,
                                  java.util.List<Map<String, String>> dataList,
                                  String nameField) throws IOException {
        File dir = new File(outputDir);
        if (!dir.exists()) {
            dir.mkdirs();
        }

        // 一次性读取模板到内存,复用解析结果
        byte[] templateBytes = java.nio.file.Files.readAllBytes(
                java.nio.file.Paths.get(templatePath));

        for (int i = 0; i < dataList.size(); i++) {
            Map<String, String> data = dataList.get(i);
            String fileName = data.getOrDefault(nameField, String.valueOf(i + 1));
            String outputPath = outputDir + File.separator + fileName + ".pdf";

            // 每次从字节数组加载(避免重复解析模板文件)
            try (PDDocument document = Loader.loadPDF(templateBytes)) {
                PDAcroForm acroForm = document.getDocumentCatalog().getAcroForm();
                for (Map.Entry<String, String> entry : data.entrySet()) {
                    PDField field = acroForm.getField(entry.getKey());
                    if (field != null) {
                        field.setValue(entry.getValue() != null ? entry.getValue() : "");
                    }
                }
                acroForm.flatten();
                document.save(new File(outputPath));
            }
        }
    }
}

5.3 PDFBox 3.0 vs 2.x 主要变化

功能PDFBox 2.xPDFBox 3.0
加载文档PDDocument.load(file)Loader.loadPDF(file)
依赖模块单一 pdfboxpdfbox + pdfbox-io
Java 版本Java 8+Java 11+
表单扁平化acroForm.flatten()同上(API 兼容)
性能基准显著提升(内存优化)

六、方案四:OpenPDF 实现(iText 5.x 兼容替代)

6.1 依赖配置

<dependencies>
    <!-- OpenPDF - iText 5.x 的开源替代(LGPL/MPL) -->
    <dependency>
        <groupId>com.github.librepdf</groupId>
        <artifactId>openpdf</artifactId>
        <version>3.0.4</version>
    </dependency>
</dependencies>

6.2 表单填充完整代码

import com.lowagie.text.pdf.AcroFields;
import com.lowagie.text.pdf.BaseFont;
import com.lowagie.text.pdf.PdfReader;
import com.lowagie.text.pdf.PdfStamper;

import java.io.File;
import java.io.FileOutputStream;
import java.util.List;
import java.util.Map;

/**
 * PDF 表单填充工具类(基于 OpenPDF)
 * API 与 iText 5.x 几乎完全一致
 */
public class OpenPdfUtil {

    private static final String FONT_PATH = "/fonts/simsun.ttc";

    /**
     * 填充 PDF 表单
     */
    public static void fillPdf(String templatePath,
                                String outputPath,
                                Map<String, String> fieldData) throws Exception {
        PdfReader reader = new PdfReader(templatePath);
        PdfStamper stamper = new PdfStamper(reader, new FileOutputStream(outputPath));

        try {
            AcroFields form = stamper.getAcroFields();

            // 嵌入中文字体
            BaseFont bfChinese = BaseFont.createFont(
                    FONT_PATH + ",0",
                    BaseFont.IDENTITY_H,
                    BaseFont.EMBEDDED
            );
            form.addSubstitutionFont(bfChinese);

            // 填充字段
            for (Map.Entry<String, String> entry : fieldData.entrySet()) {
                String key = entry.getKey();
                String value = entry.getValue();
                if (value == null) value = "";

                // 判断字段类型
                int fieldType = form.getFieldType(key);
                if (fieldType == AcroFields.FIELD_TYPE_CHECKBOX) {
                    // Checkbox
                    form.setField(key, "Yes".equals(value) ? "Yes" : "Off");
                } else if (fieldType == AcroFields.FIELD_TYPE_RADIOBUTTON) {
                    form.setField(key, value);
                } else {
                    form.setField(key, value);
                }
            }

            // 表单域转静态内容(必须!)
            stamper.setFormFlattening(true);
        } finally {
            stamper.close();
            reader.close();
        }
    }

    /**
     * 批量填充(复用模板字节数组)
     */
    public static void batchFill(byte[] templateBytes,
                                  String outputDir,
                                  List<Map<String, String>> dataList) throws Exception {
        for (int i = 0; i < dataList.size(); i++) {
            PdfReader reader = new PdfReader(templateBytes);
            String outputPath = outputDir + File.separator + "doc_" + i + ".pdf";
            PdfStamper stamper = new PdfStamper(reader, new FileOutputStream(outputPath));

            AcroFields form = stamper.getAcroFields();
            BaseFont bfChinese = BaseFont.createFont(
                    FONT_PATH + ",0", BaseFont.IDENTITY_H, BaseFont.EMBEDDED);
            form.addSubstitutionFont(bfChinese);

            for (Map.Entry<String, String> entry : dataList.get(i).entrySet()) {
                form.setField(entry.getKey(), entry.getValue());
            }

            stamper.setFormFlattening(true);
            stamper.close();
            reader.close();
        }
    }
}

6.3 从 iText 5.x 迁移

部分基础类的包名与用法接近,但迁移仍要逐项核对表单、字体、加密、签名和布局行为:

// iText 5.x
import com.itextpdf.text.pdf.BaseFont;
import com.itextpdf.text.pdf.PdfReader;
import com.itextpdf.text.pdf.PdfStamper;
import com.itextpdf.text.pdf.AcroFields;

// OpenPDF(只改包名)
import com.lowagie.text.pdf.BaseFont;
import com.lowagie.text.pdf.PdfReader;
import com.lowagie.text.pdf.PdfStamper;
import com.lowagie.text.pdf.AcroFields;
从 iText 5.x 迁移到 OpenPDF
  1. 替换 Maven 依赖(com.itextpdf:itextpdfcom.github.librepdf:openpdf
  2. 全局替换包名(com.itextpdfcom.lowagie
  3. 大部分代码无需修改
  4. 测试表单填充、中文显示、Checkbox 等关键功能
OpenPDF 3.x 升级说明

OpenPDF 从 2.x 升级到 3.x 主要变化:

  • 更低 JDK 版本提升到 JDK 11
  • 内部依赖更新(BouncyCastle 1.84、icu4j 78.3 等)
  • API 基本兼容,从 2.x 迁移只需修改版本号
  • 如果项目仍使用 JDK 8,继续使用 OpenPDF 2.x 末尾的 2.0.x 版本

七、方案五:Flying Saucer(HTML → PDF)

如果你更习惯用 HTML/CSS 写模板,可以用 Flying Saucer 直接把 HTML 转成 PDF。

依赖

<dependency>
    <groupId>org.xhtmlrenderer</groupId>
    <artifactId>flying-saucer-pdf-openpdf</artifactId>
    <version>9.3.1</version>
</dependency>

代码

import com.lowagie.text.pdf.BaseFont;
import org.xhtmlrenderer.pdf.ITextRenderer;

import java.io.File;
import java.io.FileOutputStream;

public class HtmlToPdfUtil {

    public static void convert(String htmlPath, String pdfPath) throws Exception {
        ITextRenderer renderer = new ITextRenderer();

        // 注册中文字体
        renderer.getFontResolver().addFont(
                "/fonts/simsun.ttc",
                BaseFont.IDENTITY_H,
                BaseFont.EMBEDDED
        );

        renderer.setDocument(new File(htmlPath));
        renderer.layout();
        try (FileOutputStream out = new FileOutputStream(pdfPath)) {
            renderer.createPDF(out);
        }
    }
}

HTML 模板示例

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8"/>
    <style>
        body { font-family: SimSun; font-size: 12pt; }
        .header { text-align: center; font-size: 18pt; font-weight: bold; }
        .field { color: #333; border-bottom: 1px solid #000; }
        table { width: 100%; border-collapse: collapse; }
        td, th { border: 1px solid #000; padding: 6px; }
    </style>
</head>
<body>
    <div class="header">商品购买合同</div>
    <p>甲方:<span class="field">${buyerName}</span></p>
    <p>乙方:<span class="field">${sellerName}</span></p>
    <table>
        <tr><th>商品</th><th>数量</th><th>单价</th></tr>
        <tr><td>${productName}</td><td>${quantity}</td><td>${price}</td></tr>
    </table>
</body>
</html>
Flying Saucer 适用场景
  • 模板是 HTML/CSS,前端同学也能参与模板设计
  • 需要复杂排版(表格、图片、多列布局)
  • 从 Web 页面导出 PDF 的场景
  • 局限:不支持 JavaScript,CSS 支持不完整(Flex/Grid 不支持)

八、方案六:其他方案速览

方案适用场景特点
JasperReports复杂报表报表引擎,支持 PDF/Excel/CSV,LGPL 许可证
Thymeleaf + Flying Saucer动态 HTML → PDFThymeleaf 模板引擎 + Flying Saucer 渲染
wkhtmltopdf通用 HTML → PDF命令行工具,Java 通过 ProcessBuilder 调用
Puppeteer / Playwright复杂网页截图/导出Node.js 方案,支持 JavaScript 渲染
docx4jWord → PDF直接把 Word 文档转 PDF,保留格式更稳妥

docx4j(Word → PDF 优先方案)

<dependency>
    <groupId>org.docx4j</groupId>
    <artifactId>docx4j-JAXB-ReferenceImpl</artifactId>
    <version>11.4.9</version>
</dependency>
import org.docx4j.Docx4J;
import org.docx4j.openpackaging.packages.WordprocessingMLPackage;

import java.io.File;
import java.io.FileOutputStream;

public class WordToPdfUtil {

    public static void convert(String docxPath, String pdfPath) throws Exception {
        WordprocessingMLPackage word = WordprocessingMLPackage.load(new File(docxPath));
        try (FileOutputStream out = new FileOutputStream(pdfPath)) {
            Docx4J.toPDF(word, out);
        }
    }
}
Word → PDF 场景推荐
如果你的模板本身就是 Word 文档,不要用 iText 重新排版,直接用 docx4j 转 PDF 效果更稳妥,格式保留更完整。

九、性能验证

PDF 方案不能用一组脱离模板的数据排名。基准应使用项目中的真实模板,固定字体、图片、表格、签名、输出页数和并发度,分别测量预热后单份延迟、批量吞吐、内存峰值、输出大小与渲染正确率。测试还要记录库版本、JDK、字体文件和许可证模式。

性能结论
  • AcroForm 模板优先比较字段、字体和扁平化支持
  • 从底层绘制 PDF 时比较排版能力和维护成本
  • HTML 模板验证 CSS 支持、分页和字体嵌入
  • Word 模板验证转换引擎在目标系统上的还原度
  • 许可证需要按当前版本和发布方式让合规人员确认

十、踩坑记录

坑1:中文显示为方块

原因: 未嵌入中文字体。PDF 默认使用 Helvetica 等西文字体,不认识中文。

解决: 必须调用 addSubstitutionFont()setFont() 指定中文字体。

坑2:TTC 文件加载失败

原因: TTC 是字体集合,不指定索引会报错。

解决: 路径后加 ,0(如 simsun.ttc,0)。

坑3:模板中的域找不到

原因: Word 转 PDF 时,表单域名称可能被自动修改。

解决:form.getFields().keySet() 打印所有域名,核对后修改代码。

坑4:Checkbox 值对不上

原因: 不同工具生成的 Checkbox,选中值可能是 Yes1Ontrue

解决: 打印表单域的 getAppearanceStates() 查看可用值。

坑5:模板打开后空白

原因: 使用了 PdfStamper 但没调 setFormFlattening(true),表单域数据还在但没渲染。

解决: 一定要调 setFormFlattening(true) 将表单域转为静态内容。

坑6:批量生成内存溢出

原因: 大批量生成时,每个 PdfReader 都会解析模板,内存持续增长。

解决: 将模板读入 byte[],每次从字节数组创建 Reader,复用解析结果。


十一、总结与成熟实践

11.1 模板设计规范

  1. 命名规范:表单域使用英文驼峰命名,如 userNamepolicyNo
  2. 字体预留:模板中表单域的字号要与实际填充字号一致
  3. 版本管理:模板文件纳入 Git 管理,变更时记录版本号
  4. 测试模板:准备一个"全字段测试模板”,覆盖所有字段类型

11.2 字体嵌入策略

// 生产环境推荐:将字体文件放在 resources/fonts/ 下
// 优点:随项目打包,不依赖服务器字体
// 缺点:增加包体积(宋体约 10MB)

// 备选方案:放在固定路径(如 /data/fonts/)
// 优点:不增加包体积
// 缺点需要运维确保文件存在

11.3 批量生成性能优化

/**
 * 优化建议:
 * 1. 复用模板字节数组 —— 每次读文件会增加 IO 开销
 * 2. 使用字节数组流 —— 减少磁盘 IO
 * 3. 线程池并行 —— 注意每个线程独立的文档实例
 */
public static void batchFillOptimized(String templatePath,
                                        String outputDir,
                                        List<Map<String, String>> dataList) throws Exception {
    // 一次性读取模板到内存
    byte[] templateBytes = Files.readAllBytes(Paths.get(templatePath));

    // 使用线程池并行生成
    ExecutorService pool = Executors.newFixedThreadPool(
            Runtime.getRuntime().availableProcessors()
    );

    List<Future<?>> futures = new ArrayList<>();
    for (int i = 0; i < dataList.size(); i++) {
        final int index = i;
        futures.add(pool.submit(() -> {
            try {
                // 每个线程独立创建 Reader(线程安全)
                PdfReader reader = new PdfReader(templateBytes);
                String outputPath = outputDir + "/doc_" + index + ".pdf";
                PdfStamper stamper = new PdfStamper(reader, new FileOutputStream(outputPath));
                // ... 填充逻辑同上
                stamper.setFormFlattening(true);
                stamper.close();
                reader.close();
            } catch (Exception e) {
                throw new RuntimeException("生成第 " + index + " 份 PDF 失败", e);
            }
        }));
    }

    // 等待全部完成
    for (Future<?> f : futures) {
        f.get();
    }
    pool.shutdown();
}

11.4 文件存储方案

方案适用场景优缺点
本地磁盘单机部署简单直接,不支持分布式
MinIO / FastDFS集群部署分布式、高可用
OSS(阿里云/腾讯云)云原生省运维,按量付费
数据库 BLOB小文件、低频不推荐大文件

推荐架构: 生成 → 临时目录 → 上传 OSS → 删除临时文件 → 返回 URL

11.5 选型决策树

需要生成/填充 PDF?
├── 是
│   ├── 商业项目 + 零预算?
│   │   ├── 是 → PDFBox 3.0(Apache 2.0)
│   │   └── 否 → iText 9.x 商业版
│   ├── 从 iText 5.x 迁移?
│   │   └── 是 → OpenPDF 3.0(改包名即可)
│   ├── 开源项目?
│   │   └── 是 → iText 9.x(功能最强)
│   └── 只需简单表单填充?
│       └── 是 → PDFBox 3.0(最安全)
└── 否 → 不需要 PDF 库

参考资料


系列导航: Java 工程化实战系列 | 下一篇:Java 后端开发常用工具类与代码片段精选