PDF 模板生成实战:iText 5.x/9.x 方案对比与开源替代
📅 2026-05-15 | 🏷️ Java, PDF, iText, OpenPDF, PDFBox | 📖 阅读约 15 分钟
一、背景与问题
在企业级应用中,大量业务文档需要模板化生成:保单、合同、告知书、发票、审批单……这些文档有三个共同特征:
- 格式固定 — 排版、字体、Logo 位置都是确定的
- 内容动态 — 姓名、金额、日期等字段每次不同
- 批量生产 — 高峰期可能一次生成数千份
常见做法是:先用 Word 制作模板 → 用 Java 填充数据 → 输出 PDF。
本文对比两种主流方案,给出完整代码和踩坑记录。
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.x | iText 9.x | PDFBox 3.0 | OpenPDF 3.0 |
|---|---|---|---|---|
| 当前公开版本 | 5.5.13.4 | 9.6.0 | 3.0.7 (2026-03) | 3.0.4 |
| 许可证 | AGPL v3(5.5.13+) | AGPL v3 | Apache 2.0 | LGPL / MPL |
| 商用免费 | ⚠️ 需开源或购买授权 | ⚠️ 需开源或购买授权 | ✅ 完全免费 | ✅ 完全免费 |
| 表单填充 | ✅ 优秀 | ✅ 优秀 | ✅ 支持 | ✅ 基于 iText 5.x fork |
| 中文支持 | ✅ 需嵌入字体 | ✅ 需嵌入字体 | ✅ 需嵌入字体 | ✅ 需嵌入字体 |
| PDF 2.0 | ❌ | ✅ 原生支持 | ✅ | ✅ |
| 社区活跃度 | 低(维护模式) | 高(官方主推) | 高(Apache 顶级项目) | 中(社区维护) |
| API 易用性 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ |
| 学习资源 | 丰富 | 丰富 | 中等 | 较少 |
2.2 许可证问题详解
iText 的 AGPL v3 意味着什么?
- 如果你的软件以 AGPL 发布,可以免费使用
- 如果是闭源商业软件,必须购买商业授权(iText 商业版按年收费,价格不菲)
- 所谓"内网规避"在法律上有争议,不建议依赖,AGPL 的触发条件是"通过网络与用户交互",企业内部系统也可能被认定为触发场景
- 即使是纯内部使用,如果代码分发给第三方(如外包、子公司),也需要合规
- 法律建议:如果你的项目不是 AGPL 开源的,请直接选择 PDFBox 或 OpenPDF
开源替代方案详解:
| 方案 | 许可证 | 特点 | 推荐场景 |
|---|---|---|---|
| Apache PDFBox 3.0 | Apache 2.0 | 更安全的许可证,Apache 顶级项目,功能全面 | 商业项目首选,特别是表单填充、文本提取 |
| OpenPDF 3.0 | LGPL/MPL | iText 4.x 的社区 fork,API 与 iText 5.x 高度兼容 | 从 iText 5.x 迁移成本更低 |
| Flying Saucer (OpenPDF-html) | LGPL | HTML → PDF 转换 | 需要从 HTML 生成 PDF 的场景 |
| JasperReports | LGPL | 报表引擎,支持多种输出格式 | 复杂报表场景 |
选型建议:
| 场景 | 推荐方案 |
|---|---|
| 开源项目 | iText 9.x(功能高强度) |
| 商业项目、预算充足 | iText 9.x 商业版 |
| 商业项目、零预算 | PDFBox 3.0(Apache 2.0,更安全) |
| 从 iText 5.x 迁移 | OpenPDF 3.0(API 兼容) |
| 只需要简单表单填充 | PDFBox(Apache 许可,更安全) |
| HTML → PDF | Flying 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 模板制作
制作流程:
- 在 Word 中设计文档排版
- 在需要动态填充的位置插入 表单域(开发工具 → 表单域)
- 给每个表单域命名(如
userName、policyNo) - 另存为 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 | 屏幕显示优化 |
- 版权问题:微软雅黑、宋体等字体商用需要授权。思源黑体(Source Han Sans)和思源宋体(Source Han Serif)是免费商用的替代方案。
- 文件大小:嵌入中文字体后 PDF 体积会显著增大(每个字体约 5-15MB),可通过子集化(subset)减小体积。
- 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.x | iText 9.x |
|---|---|---|
| 核心类 | PdfReader + PdfStamper | PdfDocument |
| 表单操作 | AcroFields | PdfAcroForm |
| 字体加载 | BaseFont.createFont() | PdfFontFactory.createFont() |
| 扁平化 | stamper.setFormFlattening(true) | form.flattenFields() |
| 关闭资源 | 需手动 close reader + stamper | pdfDoc.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 整体兼容 |
| 更低 JDK | JDK 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>- iText 9.x 要求 JDK 11+,JDK 8 项目无法使用
- 部分内部 API 有变化,但表单填充(
PdfAcroForm)、字体加载(PdfFontFactory)等公开 API 基本兼容 - 建议升级后重新测试中文显示和表单填充功能
- 如果项目无法升级到 JDK 11,继续使用 iText 7.x 末尾的 7.2.x 版本
五、方案三:PDFBox 3.0 实现(开源替代)
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.x | PDFBox 3.0 |
|---|---|---|
| 加载文档 | PDDocument.load(file) | Loader.loadPDF(file) |
| 依赖模块 | 单一 pdfbox | pdfbox + 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;- 替换 Maven 依赖(
com.itextpdf:itextpdf→com.github.librepdf:openpdf) - 全局替换包名(
com.itextpdf→com.lowagie) - 大部分代码无需修改
- 测试表单填充、中文显示、Checkbox 等关键功能
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>- 模板是 HTML/CSS,前端同学也能参与模板设计
- 需要复杂排版(表格、图片、多列布局)
- 从 Web 页面导出 PDF 的场景
- 局限:不支持 JavaScript,CSS 支持不完整(Flex/Grid 不支持)
八、方案六:其他方案速览
| 方案 | 适用场景 | 特点 |
|---|---|---|
| JasperReports | 复杂报表 | 报表引擎,支持 PDF/Excel/CSV,LGPL 许可证 |
| Thymeleaf + Flying Saucer | 动态 HTML → PDF | Thymeleaf 模板引擎 + Flying Saucer 渲染 |
| wkhtmltopdf | 通用 HTML → PDF | 命令行工具,Java 通过 ProcessBuilder 调用 |
| Puppeteer / Playwright | 复杂网页截图/导出 | Node.js 方案,支持 JavaScript 渲染 |
| docx4j | Word → 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);
}
}
}九、性能验证
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,选中值可能是 Yes、1、On、true。
解决: 打印表单域的 getAppearanceStates() 查看可用值。
坑5:模板打开后空白
原因: 使用了 PdfStamper 但没调 setFormFlattening(true),表单域数据还在但没渲染。
解决: 一定要调 setFormFlattening(true) 将表单域转为静态内容。
坑6:批量生成内存溢出
原因: 大批量生成时,每个 PdfReader 都会解析模板,内存持续增长。
解决: 将模板读入 byte[],每次从字节数组创建 Reader,复用解析结果。
十一、总结与成熟实践
11.1 模板设计规范
- 命名规范:表单域使用英文驼峰命名,如
userName、policyNo - 字体预留:模板中表单域的字号要与实际填充字号一致
- 版本管理:模板文件纳入 Git 管理,变更时记录版本号
- 测试模板:准备一个"全字段测试模板”,覆盖所有字段类型
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 后端开发常用工具类与代码片段精选