乐于分享
好东西不私藏

Java 高效生成 PDF 实战指南:从基础到动态模板

Java 高效生成 PDF 实战指南:从基础到动态模板

大家好,我是一安~

引言

在业务开发中,电子发票、订单凭证、合同文件等场景常需生成PDF供用户预览、下载或打印。本文将摒弃冗余细节,聚焦核心实现,带你快速掌握Java生成PDF的关键技巧,以主流工具iText为核心,覆盖基础生成、HTML转换及动态内容填充三大核心场景。

一、核心工具:iText 框架简介

iTextJava生态中成熟的PDF处理类库,支持直接生成PDF、HTML/XMLPDF等核心功能,兼容中文渲染与CSS样式解析,分为iText5(稳定常用)和iText7(重构优化版),日常开发中基础API用法一致,无需过度纠结版本选择。

1.1 iText5 vs iText7

特性
iText5
iText7
发布时间
较早,稳定成熟
重构优化版
学习成本
较低,文档丰富
稍高,API有变化
社区支持
丰富,案例多
逐步增加
推荐场景
一般业务场景
复杂PDF处理

二、快速上手

2.1 引入核心依赖

<dependencies><!-- iText 核心包 --><dependency><groupId>com.itextpdf</groupId><artifactId>itextpdf</artifactId><version>5.5.11</version></dependency><!-- HTML 转 PDF 工具 --><dependency><groupId>com.itextpdf.tool</groupId><artifactId>xmlworker</artifactId><version>5.5.11</version></dependency><!-- 中文显示支持 --><dependency><groupId>com.itextpdf</groupId><artifactId>itext-asian</artifactId><version>5.2.0</version></dependency><!-- CSS 样式渲染 --><dependency><groupId>org.xhtmlrenderer</groupId><artifactId>flying-saucer-pdf-itext5</artifactId><version>9.1.16</version></dependency></dependencies>

2.2 依赖说明

依赖
作用
必要性
itextpdf
iText核心库,提供PDF生成基础功能
必须
xmlworker
HTML/XML转PDF功能
按需引入
itext-asian
中文字体支持,解决中文乱码
必须
flying-saucer-pdf-itext5
CSS样式渲染支持
按需引入

三、基础 PDF 生成

3.1 核心步骤

适用于简单文本类PDF,核心步骤为:创建文档对象 → 配置写入器 → 设置字体 → 写入内容 → 关闭文档

3.2 代码示例

publicclassBasicPdfGenerator{publicstaticvoidmain(String[] args)throws Exception {// 1. 初始化 A4 尺寸文档        Document document = new Document(PageSize.A4);// 2. 绑定输出流(生成 hello.pdf 文件)        PdfWriter.getInstance(document, new FileOutputStream("hello.pdf"));// 3. 配置中文字体(解决中文乱码)        BaseFont chineseFont = BaseFont.createFont("STSong-Light""UniGB-UCS2-H", BaseFont.NOT_EMBEDDED);        Font font = new Font(chineseFont, 12, Font.NORMAL);// 4. 写入内容并关闭        document.open();        document.add(new Paragraph("PDF 基础生成示例", font));        document.add(new Paragraph("这是通过 iText 生成的简单 PDF 文档", font));        document.close();    }}

3.3 关键点说明

1. 文档尺寸

// 常用尺寸Document document = new Document(PageSize.A4);           // A4纸张Document document = new Document(PageSize.A4.rotate());  // A4横向Document document = new Document(PageSize.A5);           // A5纸张// 自定义尺寸(单位:磅,1英寸=72磅)Rectangle pageSize = new Rectangle(595842);  // 自定义宽高Document document = new Document(pageSize, 50505050);  // 边距:左、右、上、下

2. 中文字体配置

// 方式1:使用系统字体BaseFont chineseFont = BaseFont.createFont("STSong-Light""UniGB-UCS2-H", BaseFont.NOT_EMBEDDED);// 方式2:使用自定义字体文件BaseFont chineseFont = BaseFont.createFont("/fonts/simsun.ttf", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);

四、HTML 转 PDF

4.1 编写HTML模板

<htmlcharset="UTF-8"><head><style>table { border-collapse: collapse; width100%; }th { background#eaeaeaborder1px solid #000text-align: center; }td { border1px solid #000text-align: center; }.title { text-align: center; font-size24pxfont-weight: bold; letter-spacing5px; }</style></head><body><divclass="title">入库单</div><div>操作人:一安 | 创建时间:2025-12-16 10:00:00</div><table><tr><th>序号</th><th>商品名称</th><th>颜色</th><th>数量</th></tr><tr><td>1</td><td>iPhone 15</td><td></td><td>3</td></tr><tr><td>2</td><td>AirPods Pro</td><td></td><td>4</td></tr></table><!-- 支持 base64 图片或网络图片 --><imgsrc="data:image/jpeg;base64,xxx"style="width: 100px; height: 100px;" /></body></html>

4.2 HTML 转 PDF 核心代码

publicclassItextpdf{publicstaticvoidmain(String[] args)throws Exception {        String html = readHtmlTemplate("D:\\gitee\\self-learn\\01_springboot\\test-demo\\src\\main\\resources\\static\\test.html");        convert(html, "生成的入库单.pdf");    }// 读取 HTML 文件内容privatestatic String readHtmlTemplate(String filePath)throws IOException {        StringBuilder htmlContent = new StringBuilder();        BufferedReader reader = new BufferedReader(new FileReader(new File(filePath)));        String line;while ((line = reader.readLine()) != null) {            htmlContent.append(line);        }        reader.close();return htmlContent.toString();    }// 转换 HTML 为 PDFpublicstaticvoidconvert(String htmlContent, String outputPdfPath)throws Exception {// 初始化文档(设置边距)        Document document = new Document(PageSize.A4, 25251540);        PdfWriter writer = PdfWriter.getInstance(document, new FileOutputStream(outputPdfPath));        document.open();// 配置 CSS 解析器        CSSResolver cssResolver = new StyleAttrCSSResolver();// 配置中文字体处理器        CssAppliers cssAppliers = new CssAppliersImpl(new XMLWorkerFontProvider() {@Overridepublic Font getFont(String fontname, String encoding, float size, int style){try {                    BaseFont chineseFont = BaseFont.createFont("STSongStd-Light""UniGB-UCS2-H", BaseFont.NOT_EMBEDDED);returnnew Font(chineseFont, size, style);                } catch (Exception e) {returnsuper.getFont(fontname, encoding, size, style);                }            }        });// 配置 HTML 解析上下文(支持图片)        HtmlPipelineContext htmlContext = new HtmlPipelineContext(cssAppliers);        htmlContext.setTagFactory(Tags.getHtmlTagProcessorFactory());        htmlContext.setImageProvider(new AbstractImageProvider() {@Overridepublic Image retrieve(String src){try {// 处理 base64 图片if (src.startsWith("data:image")) {byte[] imgBytes = Base64.decode(src.split(",")[1]);return Image.getInstance(imgBytes);                    }// 处理网络图片if (src.startsWith("http")) {return Image.getInstance(src);                    }                } catch (Exception e) {                    e.printStackTrace();                }returnnull;            }@Overridepublic String getImageRootPath(){returnnull;            }        });// 构建转换管道并执行        Pipeline pipeline = new CssResolverPipeline(cssResolver, new HtmlPipeline(htmlContext, new PdfWriterPipeline(document, writer)));        XMLWorker worker = new XMLWorker(pipeline, true);        XMLParser parser = new XMLParser(worker);        parser.parse(new ByteArrayInputStream(htmlContent.getBytes()));        document.close();    }}

4.3 转换流程图

┌─────────────────────────────────────────────────────────────────┐│                    HTML 转 PDF 流程                              │├─────────────────────────────────────────────────────────────────┤│                                                                 ││  1. 读取 HTML 模板文件                                          ││     ↓                                                           ││  2. 初始化 Document 和 PdfWriter                                ││     ↓                                                           ││  3. 配置 CSS 解析器(StyleAttrCSSResolver)                      ││     ↓                                                           ││  4. 配置中文字体处理器(XMLWorkerFontProvider)                   ││     ↓                                                           ││  5. 配置 HTML 解析上下文(HtmlPipelineContext)                   ││     → 支持图片处理(base64/网络图片)                            ││     ↓                                                           ││  6. 构建转换管道(Pipeline)                                     ││     → CssResolverPipeline → HtmlPipeline → PdfWriterPipeline    ││     ↓                                                           ││  7. 解析 HTML 并生成 PDF                                        ││     ↓                                                           ││  8. 关闭文档                                                     ││                                                                 │└─────────────────────────────────────────────────────────────────┘

五、动态内容填充

5.1 简单变量替换

HTML中定义${变量名}占位符,读取HTML后替换为实际数据:

HTML模板片段

<div>您好:${username}</div><div>订单号:${orderNo}</div><div>订单金额:${amount}</div><div>创建时间:${createTime}</div>

Java替换逻辑

// 替换逻辑String htmlTemplate = readHtmlTemplate("模板.html");htmlTemplate = htmlTemplate.replace("${username}""张三")                          .replace("${orderNo}""ORDER_2024001")                          .replace("${amount}""999.00")                          .replace("${createTime}""2024-01-15 10:30:00");convert(htmlTemplate, "动态订单.pdf");

5.2 使用Freemarker模板引擎

复杂场景(如动态表格、循环数据)推荐使用Freemarker,先定义模板,再注入数据模型。

Step 1:引入Freemarker依赖

<dependency><groupId>org.freemarker</groupId><artifactId>freemarker</artifactId><version>2.3.31</version></dependency>

Step 2:编写Freemarker模板

<htmlcharset="UTF-8"><head><style>table { border-collapse: collapse; width100%; }th { background#eaeaeaborder1px solid #000; }td { border1px solid #000text-align: center; }.title { text-align: center; font-size24pxfont-weight: bold; }</style></head><body><divclass="title">${title}</div><div>订单号:${orderNo} | 创建时间:${createTime}</div><table><tr><th>序号</th><th>商品名称</th><th>单价</th><th>数量</th><th>小计</th></tr><#listitemsasitem><tr><td>${item_index + 1}</td><td>${item.name}</td><td>${item.price}</td><td>${item.quantity}</td><td>${item.price * item.quantity}</td></tr></#list><tr><tdcolspan="4"style="text-align: right;">合计:</td><td>${totalAmount}</td></tr></table></body></html>

Step 3:Freemarker渲染代码

publicclassFreemarkerPdfGenerator{publicstaticvoidmain(String[] args)throws Exception {// 1. 配置Freemarker        Configuration cfg = new Configuration(Configuration.VERSION_2_3_31);        cfg.setDirectoryForTemplateLoading(new File("templates/"));        cfg.setDefaultEncoding("UTF-8");// 2. 准备数据模型        Map<String, Object> data = new HashMap<>();        data.put("title""订单详情");        data.put("orderNo""ORDER_2024001");        data.put("createTime""2024-01-15 10:30:00");        data.put("totalAmount""2997.00");        List<Map<String, Object>> items = new ArrayList<>();        items.add(createItem("iPhone 15""5999.00"1));        items.add(createItem("AirPods Pro""1999.00"2));        items.add(createItem("充电器""399.00"1));        data.put("items", items);// 3. 渲染模板        Template template = cfg.getTemplate("order.ftl");        StringWriter writer = new StringWriter();        template.process(data, writer);        String htmlContent = writer.toString();// 4. 转换为PDF        convert(htmlContent, "订单详情.pdf");    }privatestatic Map<String, Object> createItem(String name, String price, int quantity){        Map<String, Object> item = new HashMap<>();        item.put("name", name);        item.put("price", price);        item.put("quantity", quantity);return item;    }}

六、实战场景与最佳实践

6.1 常见应用场景

场景
特点
推荐方案
电子发票
格式固定、内容动态
HTML模板 + 变量替换
订单凭证
含表格、图片
Freemarker + HTML转PDF
合同文件
复杂排版、多页
基础API + 自定义布局
报表导出
大数据量、分页
分页处理 + 流式写入

6.2 性能优化建议

1. 字体缓存

// 避免重复创建字体对象publicclassFontCache{privatestaticfinal Map<String, BaseFont> FONT_CACHE = new ConcurrentHashMap<>();publicstatic BaseFont getChineseFont(){return FONT_CACHE.computeIfAbsent("chinese", k -> {try {return BaseFont.createFont("STSong-Light""UniGB-UCS2-H", BaseFont.NOT_EMBEDDED);            } catch (Exception e) {thrownew RuntimeException("字体加载失败", e);            }        });    }}

2. 图片压缩

// 压缩图片减少PDF体积publicstatic Image compressImage(String imagePath, float quality)throws Exception {    BufferedImage image = ImageIO.read(new File(imagePath));    ByteArrayOutputStream baos = new ByteArrayOutputStream();    ImageWriter writer = ImageIO.getImageWritersByFormatName("jpg").next();    ImageWriteParam param = writer.getDefaultWriteParam();    param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);    param.setCompressionQuality(quality);    writer.setOutput(ImageIO.createImageOutputStream(baos));    writer.write(nullnew IIOImage(image, nullnull), param);    writer.dispose();return Image.getInstance(baos.toByteArray());}

3. 流式写入大数据

// 大数据量时使用PdfPTable分页PdfPTable table = new PdfPTable(5);table.setWidthPercentage(100);for (Order order : orders) {    table.addCell(order.getId());    table.addCell(order.getOrderNo());    table.addCell(order.getAmount());// 每处理1000条刷新一次if (table.getRows().size() % 1000 == 0) {        document.add(table);        table.deleteBodyRows();    }}document.add(table);

6.3 常见问题与解决方案

问题1:中文乱码

// 解决方案:使用中文字体BaseFont chineseFont = BaseFont.createFont("STSong-Light""UniGB-UCS2-H", BaseFont.NOT_EMBEDDED);Font font = new Font(chineseFont, 12, Font.NORMAL);

问题2:图片不显示

// 解决方案:配置ImageProviderhtmlContext.setImageProvider(new AbstractImageProvider() {@Overridepublic Image retrieve(String src){try {if (src.startsWith("data:image")) {byte[] imgBytes = Base64.decode(src.split(",")[1]);return Image.getInstance(imgBytes);            }if (src.startsWith("http")) {return Image.getInstance(new URL(src));            }return Image.getInstance(new File(src).toURI().toURL());        } catch (Exception e) {returnnull;        }    }@Overridepublic String getImageRootPath(){returnnew File("images/").getAbsolutePath();    }});

问题3:CSS样式不生效

// 解决方案:确保CSS写在内联或style标签中,避免外部引用// flying-saucer对CSS3支持有限,建议使用CSS2.1规范

七、面试加分项(Q&A)

Q1:iText生成PDF时,如何解决中文乱码问题?底层原理是什么?

中文乱码是因为PDF默认字体不支持中文字符。解决方案有两种方式:

  1. 使用系统字体:BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED)
  2. 使用自定义字体文件:BaseFont.createFont("/fonts/simsun.ttf", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED)

原理:PDF文件内部使用CMap(Character Map)映射表将字符编码映射到字形。中文字体包含完整的字形定义,BaseFont加载字体后会建立编码到字形的映射关系。UniGB-UCS2-H是GB2312编码的横向书写模式,IDENTITY_H是Unicode横向书写模式,后者支持更广泛的字符集。

HTML转PDF时,还需要在XMLWorkerFontProvider中配置字体,否则HTML中的中文会显示为空白或乱码。

Q2:HTML转PDF时,CSS样式不生效怎么办?flying-saucer对CSS支持有限制吗?

flying-saucer对CSS支持确实有限制,主要支持CSS2.1规范,CSS3部分特性不支持。常见问题及解决方案:

  1. 外部CSS文件不加载:将CSS写在<style>标签内或使用内联样式
  2. CSS3特性不支持:如flexgridtransform等,需要改用CSS2.1的floattable布局
  3. 伪类选择器问题:before:after支持有限,建议用真实元素替代
  4. 单位问题:支持pxptem%,不支持remvwvh

实际开发中,建议:

  • 使用简单的CSS2.1规范编写样式
  • 避免复杂的嵌套和浮动布局
  • 表格布局是最稳定的选择
  • 如果样式复杂,可以考虑用基础API直接绘制

Q3:生成PDF时,如何处理分页问题?特别是表格跨页显示?

iText提供了多种分页控制机制:

1. 自动分页文档内容超出页面高度时,iText会自动创建新页面。可以通过document.newPage()手动分页。

2. 表格分页控制

PdfPTable table = new PdfPTable(3);table.setSplitLate(false);  // 行不跨页分割table.setSplitRows(true);   // 允许行跨页(默认true)table.setHeaderRows(1);     // 设置表头行数,跨页自动重复

3. 保持内容完整性

table.setKeepTogether(true);  // 表格整体不分页(适合小表格)

4. 分页事件监听

writer.setPageEvent(new PdfPageEventHelper() {@OverridepublicvoidonEndPage(PdfWriter writer, Document document){// 添加页眉页脚、页码等 }});

Q4:PDF生成性能如何优化?大数据量报表导出有什么方案?

PDF生成是CPU和IO密集型操作,优化策略如下:

1. 字体缓存BaseFont创建开销大,使用静态变量或缓存池复用字体对象。

2. 图片优化压缩图片质量、控制分辨率、使用WebP格式减小体积。

3. 流式写入大数据量时使用PdfPTable分批写入,避免内存溢出:

4. 异步生成耗时PDF生成放到异步线程,生成完成后通知用户下载。

5. 模板预编译复杂HTML模板可以预编译缓存,减少重复解析开销。

大数据量报表建议:分页查询 + 分批写入 + 异步生成 + 压缩下载。

Q5:iText的AGPL许可证对商业项目有什么影响?有替代方案吗?

iText采用双许可模式:

AGPL许可证(免费)

  • 开源项目可以使用
  • 商业项目如果使用,必须将整个项目开源
  • 不适合闭源商业软件

商业许可证(付费)

  • 购买后可闭源使用
  • 费用较高,按开发者数量收费

替代方案:

  1. Apache PDFBox

    • Apache 2.0许可证,完全免费
    • 功能较iText弱,HTML转PDF支持不好
    • 适合基础PDF操作
  2. OpenPDF

    • iText的分支,LGPL/MPL许可证
    • 兼容iText 4.x API
    • 商业项目可用,推荐替代方案
  3. Flying Saucer(Open HTML to PDF)

    • HTML转PDF专用
    • LGPL许可证,商业可用
  4. wkhtmltopdf

    • 基于WebKit的命令行工具
    • 通过Java调用系统命令
    • HTML渲染效果好,CSS支持完善

实际项目中,如果只是基础PDF生成,推荐OpenPDF;如果需要HTML转PDF,推荐Open HTML to PDF或wkhtmltopdf。


参考资源

  • iText官方文档
  • iText 5 API文档
  • Flying Saucer GitHub
  • OpenPDF GitHub

📖 往期推荐

▸ Spring Boot 接口安全设计:接口限流、防重放攻击与签名验证实战

▸ 基于 Java 的工作日与节假日智能识别

▸ 基于对象池模式的 JSON 处理性能提升实践

如果觉得有帮助,欢迎转发给需要的朋友 💙

有问题评论区见 ✨