如何使用Java开发在线生成 pdf 文档 ?
01
一、场景背景与技术选型
在后端业务开发过程中,研发人员经常会遇到这类需求:向用户提供标准化的电子凭证类文件,比如网银/支付宝/微信支付的电子发票、订单出库单、电子签章合同等。这类文件需要支持用户预览、打印、下载,而PDF格式是当前最适配这类需求的解决方案——它能保证跨设备、跨平台的格式一致性,不会因终端不同导致排版错乱。本文将围绕Java技术栈,详细讲解如何基于iText组件实现PDF文档的在线生成,从基础的文本写入到复杂的HTML转PDF场景,覆盖实际开发中最常用的核心玩法。
二、核心技术:iText组件介绍
iText是SourceForge开源社区的经典Java类库,专门用于PDF文档的生成与处理。它的核心能力包括:
-
• 直接生成PDF/RTF文档; -
• 支持XML、HTML文件转换为PDF; -
• 提供丰富的API控制PDF排版、字体、图片等元素。目前iText有两个主流版本:iText5和iText7。iText5是应用最广泛的版本,由大量社区开发者贡献代码,优点是资料多、易上手;iText7是官方对iText5的重构版本,架构更规范,但API变动较大。实际开发中,基础场景下两者的核心用法差异不大,本文以iText5为例展开讲解。
三、基础实现:环境准备与HelloWorld
3.1 引入Maven依赖
首先需要在项目的pom.xml中引入iText相关依赖,重点包含核心包、中文支持包、HTML转PDF辅助包等:
<dependencies> <!-- iText核心包:PDF生成核心能力 --> <dependency> <groupId>com.itextpdf</groupId> <artifactId>itextpdf</artifactId> <version>5.5.11</version> </dependency> <!-- XML/HTML转PDF的辅助工具包 --> <dependency> <groupId>com.itextpdf.tool</groupId> <artifactId>xmlworker</artifactId> <version>5.5.11</version> </dependency> <!-- iText中文支持包:解决中文乱码问题 --> <dependency> <groupId>com.itextpdf</groupId> <artifactId>itext-asian</artifactId> <version>5.2.0</version> </dependency> <!-- HTML渲染辅助包:优化HTML转PDF的排版 --> <dependency> <groupId>org.xhtmlrenderer</groupId> <artifactId>flying-saucer-pdf-itext5</artifactId> <version>9.1.16</version> </dependency> <!-- HTML格式化工具包:修复不规范HTML标签 --> <dependency> <groupId>net.sf.jtidy</groupId> <artifactId>jtidy</artifactId> <version>r938</version> </dependency></dependencies>
3.2 最简示例:生成HelloWorld PDF
下面是最基础的PDF生成代码,实现写入“hello world”并生成PDF文件,代码中添加详细注解便于理解:
import com.itextpdf.text.*;import com.itextpdf.text.pdf.BaseFont;import com.itextpdf.text.pdf.PdfWriter;import java.io.FileOutputStream;public class CreatePDFMainTest { public static void main(String[] args) throws Exception { // 1. 创建Document对象,指定页面大小为A4 Document document = new Document(PageSize.A4); // 2. 绑定PDF写入器:将Document内容输出到指定文件(hello.pdf) PdfWriter.getInstance(document, new FileOutputStream("hello.pdf")); // 3. 加载中文字体:解决中文乱码问题 // STSong-Light:宋体;UniGB-UCS2-H:中文编码格式;NOT_EMBEDDED:不嵌入字体文件(减小PDF体积) BaseFont bfchinese = BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED); // 创建字体对象:指定字体、字号、样式(NORMAL为普通样式) Font fontChinese = new Font(bfchinese, 12, Font.NORMAL); // 4. 打开文档:开始写入内容前必须打开 document.open(); // 5. 创建段落对象:传入要写入的文本和字体 Paragraph paragraph = new Paragraph("hello world", fontChinese); // 将段落添加到文档中 document.add(paragraph); // 6. 关闭文档:释放资源,完成PDF生成 document.close(); }}
运行上述代码后,会在项目根目录生成“hello.pdf”文件,打开后可看到“hello world”文本正常显示(无中文乱码)。
四、进阶实战:HTML转PDF(适配复杂业务场景)
实际业务中,PDF的内容往往包含表格、图片、动态数据(比如入库单、发票),直接通过API写入文本/表格的方式开发效率低、维护成本高。更优的方案是:先编写HTML模板(包含动态变量),再将HTML转换为PDF——这种方式既能利用HTML的灵活排版,又能快速适配业务变更。
4.1 编写HTML模板(以入库单为例)
首先创建printDemo.html文件,模拟入库单的排版(包含表格、图片、固定文本):
<html><head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <title>入库单</title></head><body> <div> <!-- 入库单头部:标题、操作人、创建时间、二维码 --> <div> <table width="100%" border="0" cellspacing="0" cellpadding="0"> <tbody> <tr> <td height="40" colspan="2"> <h3 style="font-weight: bold; text-align: center; letter-spacing: 5px; font-size: 24px;">入库单</h3> </td> <!-- Base64格式图片:避免图片路径依赖 --> <td width="12%" height="20" rowspan="2"> <img style="width: 105px;height: 105px;" src="data:image/jpeg;base64,iVBORw0KGgoAAAANSUhEUgAAAH0AAAB9AQAAAACn+1GIAAAAqElEQVR42u3VMQ7DMAwDQP6A" /> </td> </tr> <tr> <td width="50%" height="30">操作人:xxx</td> <td width="50%" height="30" colspan="2">创建时间:2021-09-14 12:00:00</td> </tr> </tbody> </table> </div> <!-- 入库单表格:商品列表 --> <div style="margin-top: 5px; margin-bottom: 6px; margin-left: 4px"></div> <div> <table width="100%" style="border-collapse: collapse; border-spacing: 0;border:0px;"> <!-- 表格头部 --> <tr style="height: 25px;"> <td style="background: #eaeaea; text-align: center; border-left: 1px solid #000000; border-top: 1px solid #000000;" width="10%">序号</td> <td style="background: #eaeaea; text-align: center; border-left: 1px solid #000000; border-top: 1px solid #000000;" width="30%">商品</td> <td style="background: #eaeaea; text-align: center; border-left: 1px solid #000000; border-top: 1px solid #000000;" width="30%">单位</td> <td style="background: #eaeaea; text-align: center; border-left: 1px solid #000000; border-top: 1px solid #000000; border-right: 1px solid #000000;" width="30%">数量</td> </tr> <!-- 表格内容 --> <tr> <td style="text-align: center; border-left: 1px solid #000000; border-top: 1px solid #000000;">1</td> <td style="text-align: center; border-left: 1px solid #000000; border-top: 1px solid #000000;">xxx沐浴露</td> <td style="text-align: center; border-left: 1px solid #000000; border-top: 1px solid #000000;">箱</td> <td style="text-align: center; border-left: 1px solid #000000; border-top: 1px solid #000000; border-right: 1px solid #000000;">3</td> </tr> <tr> <td style="text-align: center; border-left: 1px solid #000000; border-top: 1px solid #000000;">2</td> <td style="text-align: center; border-left: 1px solid #000000; border-top: 1px solid #000000;">xxx洗发水</td> <td style="text-align: center; border-left: 1px solid #000000; border-top: 1px solid #000000;">箱</td> <td style="text-align: center; border-left: 1px solid #000000; border-top: 1px solid #000000; border-right: 1px solid #000000;">4</td> </tr> <tr> <td style="text-align: center; border-left: 1px solid #000000; border-top: 1px solid #000000;">3</td> <td style="text-align: center; border-left: 1px solid #000000; border-top: 1px solid #000000;">xxx洗衣粉</td> <td style="text-align: center; border-left: 1px solid #000000; border-top: 1px solid #000000;">箱</td> <td style="text-align: center; border-left: 1px solid #000000; border-top: 1px solid #000000; border-right: 1px solid #000000;">5</td> </tr> <tr> <td style="text-align: center; border-left: 1px solid #000000; border-top: 1px solid #000000; border-bottom: 1px solid #000000;">4</td> <td style="text-align: center; border-left: 1px solid #000000; border-top: 1px solid #000000; border-bottom: 1px solid #000000;">xxx洗面奶</td> <td style="text-align: center; border-left: 1px solid #000000; border-top: 1px solid #000000; border-bottom: 1px solid #000000;">箱</td> <td style="text-align: center; border-left: 1px solid #000000; border-top: 1px solid #000000; border-right: 1px solid #000000; border-bottom: 1px solid #000000;">5</td> </tr> </table> </div> </div></body></html>
4.2 实现HTML转PDF的核心代码
编写Java代码读取HTML文件,并将其转换为PDF,代码添加详细注解:
import com.itextpdf.text.Document;import com.itextpdf.text.PageSize;import com.itextpdf.text.pdf.*;import com.itextpdf.tool.xml.*;import com.itextpdf.tool.xml.css.CssResolver;import com.itextpdf.tool.xml.css.StyleAttrCSSResolver;import com.itextpdf.tool.xml.html.Tags;import com.itextpdf.tool.xml.html.pdfelement.Image;import com.itextpdf.tool.xml.parser.XMLParser;import com.itextpdf.tool.xml.pipeline.css.CssResolverPipeline;import com.itextpdf.tool.xml.pipeline.html.HtmlPipeline;import com.itextpdf.tool.xml.pipeline.html.HtmlPipelineContext;import com.itextpdf.tool.xml.pipeline.html.AbstractImageProvider;import com.itextpdf.tool.xml.pipeline.html.CssAppliers;import com.itextpdf.tool.xml.pipeline.html.CssAppliersImpl;import com.itextpdf.tool.xml.pipeline.pdf.PdfWriterPipeline;import java.io.*;import java.nio.charset.StandardCharsets;import java.util.Base64;public class CreatePDFMainTest { /** * 将HTML字符串转换为PDF文件 * @param htmlStr 待转换的HTML字符串 * @throws Exception 转换过程中的异常(IO/排版异常等) */ private static void writeToOutputStreamAsPDF(String htmlStr) throws Exception { // 定义生成的PDF文件路径 String targetFile = "pdfDemo.pdf"; File targetFileObj = new File(targetFile); // 如果文件已存在,先删除(避免覆盖失败) if(targetFileObj.exists()) { targetFileObj.delete(); } // 1. 创建Document对象:指定页面大小A4,设置边距(左、右、上、下) Document document = new Document(PageSize.A4, 25, 25, 15, 40); // 2. 创建PdfWriter:绑定Document和输出文件流 PdfWriter writer = PdfWriter.getInstance(document, new FileOutputStream(targetFile)); // 3. 设置PDF页眉页脚(此处为空页眉,可自定义PdfReportHeaderFooter类扩展) PdfReportHeaderFooter header = new PdfReportHeaderFooter("", 8, PageSize.A4); writer.setPageEvent(header); // 设置PDF打印缩放为“无”(适配打印场景) writer.addViewerPreference(PdfName.PRINTSCALING, PdfName.NONE); // 4. 打开文档:开始转换前必须打开 document.open(); // 5. 初始化CSS解析器:处理HTML中的CSS样式 CssResolver cssResolver = new StyleAttrCSSResolver(); // 6. 初始化字体适配:解决HTML中文字体乱码 CssAppliers cssAppliers = new CssAppliersImpl(new XMLWorkerFontProvider(){ @Override public com.itextpdf.text.Font getFont(String fontname, String encoding, boolean embedded, float size, int style, BaseColor color) { try { // 加载宋体:保证中文正常显示 BaseFont bfChinese = BaseFont.createFont("STSongStd-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED); return new com.itextpdf.text.Font(bfChinese, size, style); } catch (Exception e) { // 异常时使用默认字体 return super.getFont(fontname, encoding, size, style); } } }); // 7. 初始化HTML管道上下文:处理HTML标签和图片 HtmlPipelineContext htmlContext = new HtmlPipelineContext(cssAppliers); htmlContext.setTagFactory(Tags.getHtmlTagProcessorFactory()); // 设置图片处理器:支持Base64图片和网络图片 htmlContext.setImageProvider(new AbstractImageProvider() { @Override public Image retrieve(String src) { // 处理Base64格式图片 int pos = src.indexOf("base64,"); try { if (src.startsWith("data") && pos > 0) { // 解码Base64字符串为字节数组 byte[] imgBytes = Base64.getDecoder().decode(src.substring(pos + 7)); return Image.getInstance(imgBytes); } else if (src.startsWith("http")) { // 处理网络图片 return Image.getInstance(src); } } catch (Exception ex) { // 图片加载失败时返回null return null; } return null; } @Override public String getImageRootPath() { // 图片根路径:此处无需设置,返回null return null; } }); // 8. 构建XMLWorker管道:CSS解析 -> HTML解析 -> PDF写入 PdfWriterPipeline pdfPipeline = new PdfWriterPipeline(document, writer); HtmlPipeline htmlPipeline = new HtmlPipeline(htmlContext, pdfPipeline); CssResolverPipeline cssPipeline = new CssResolverPipeline(cssResolver, htmlPipeline); // 9. 初始化XMLWorker并执行转换 XMLWorker worker = new XMLWorker(cssPipeline, true); XMLParser parser = new XMLParser(worker); // 将HTML字符串转为字节流,指定UTF-8编码(避免中文乱码) parser.parse(new ByteArrayInputStream(htmlStr.getBytes(StandardCharsets.UTF_8))); // 10. 关闭文档:完成转换,释放资源 document.close(); } /** * 读取HTML文件内容为字符串 * @return HTML文件的字符串内容(读取失败返回null) */ private static String readHtmlFile() { StringBuffer textHtml = new StringBuffer(); try { // 读取printDemo.html文件(需确保文件在项目根目录) File file = new File("printDemo.html"); BufferedReader reader = new BufferedReader(new FileReader(file)); String tempString = null; // 逐行读取文件内容并拼接 while ((tempString = reader.readLine()) != null) { textHtml.append(tempString); } reader.close(); } catch (IOException e) { // 捕获IO异常,返回null e.printStackTrace(); return null; } return textHtml.toString(); } public static void main(String[] args) throws Exception { // 1. 读取HTML模板文件 String htmlStr = readHtmlFile(); if(htmlStr == null) { System.out.println("HTML文件读取失败!"); return; } // 2. 将HTML转换为PDF writeToOutputStreamAsPDF(htmlStr); System.out.println("PDF生成成功!"); } /** * 自定义PDF页眉页脚类(空实现,可根据需求扩展) */ static class PdfReportHeaderFooter extends PdfPageEventHelper { private String headerText; private int fontSize; private PageSize pageSize; public PdfReportHeaderFooter(String headerText, int fontSize, PageSize pageSize) { this.headerText = headerText; this.fontSize = fontSize; this.pageSize = pageSize; } // 可重写onStartPage/onEndPage方法自定义页眉页脚 @Override public void onEndPage(PdfWriter writer, Document document) { super.onEndPage(writer, document); } }}
4.3 动态数据填充(模板变量替换)
上述HTML模板中的内容是固定的,实际业务中需要动态替换变量(比如操作人、商品列表、创建时间)。核心思路:在HTML模板中定义占位符(如{createTime}),读取HTML文件后,将占位符替换为实际业务数据。示例HTML模板(含占位符):
<html><head> <meta charset="utf-8"></head><body> <div>操作人:${operator}</div> <div>创建时间:${createTime}</div> <div>商品名称:${productName}</div></body></html>
替换逻辑示例(在readHtmlFile方法后添加):
// 模拟业务数据String operator = "张三";String createTime = "2025-04-15 10:00:00";String productName = "XX品牌沐浴露";// 替换HTML中的占位符htmlStr = htmlStr.replace("${operator}", operator) .replace("${createTime}", createTime) .replace("${productName}", productName);
也可以使用Freemarker、Velocity等模板引擎实现更复杂的动态渲染(比如循环渲染商品列表),核心逻辑一致:模板+数据=最终HTML,再转PDF。
五、总结
iText框架是Java开发中生成PDF的主流选择,轻量、易用,能满足大部分业务场景的需求:
-
• 简单文本/表格PDF:直接使用iText核心API快速实现; -
• 复杂排版PDF:推荐HTML模板+转换的方式,兼顾开发效率和灵活性; -
• 中文乱码问题:核心是加载中文字体(STSong-Light/UniGB-UCS2-H); -
• 动态数据:通过占位符替换或模板引擎实现。对于超复杂的PDF场景(比如多页嵌套、复杂图表),可结合iText官方API文档扩展开发,或选择商业组件(如Aspose.PDF)。
夜雨聆风