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

引言
在业务开发中,电子发票、订单凭证、合同文件等场景常需生成PDF供用户预览、下载或打印。本文将摒弃冗余细节,聚焦核心实现,带你快速掌握Java生成PDF的关键技巧,以主流工具iText为核心,覆盖基础生成、HTML转换及动态内容填充三大核心场景。
一、核心工具:iText 框架简介
iText是Java生态中成熟的PDF处理类库,支持直接生成PDF、HTML/XML转PDF等核心功能,兼容中文渲染与CSS样式解析,分为iText5(稳定常用)和iText7(重构优化版),日常开发中基础API用法一致,无需过度纠结版本选择。
1.1 iText5 vs iText7
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
二、快速上手
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 |
|
|
xmlworker |
|
|
itext-asian |
|
|
flying-saucer-pdf-itext5 |
|
|
三、基础 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(595, 842); // 自定义宽高Document document = new Document(pageSize, 50, 50, 50, 50); // 边距:左、右、上、下
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; width: 100%; }th { background: #eaeaea; border: 1px solid #000; text-align: center; }td { border: 1px solid #000; text-align: center; }.title { text-align: center; font-size: 24px; font-weight: bold; letter-spacing: 5px; }</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, 25, 25, 15, 40); 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; width: 100%; }th { background: #eaeaea; border: 1px solid #000; }td { border: 1px solid #000; text-align: center; }.title { text-align: center; font-size: 24px; font-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 常见应用场景
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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(null, new IIOImage(image, null, null), 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默认字体不支持中文字符。解决方案有两种方式:
使用系统字体: BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED)使用自定义字体文件: 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部分特性不支持。常见问题及解决方案:
外部CSS文件不加载:将CSS写在 <style>标签内或使用内联样式CSS3特性不支持:如 flex、grid、transform等,需要改用CSS2.1的float、table布局伪类选择器问题: :before、:after支持有限,建议用真实元素替代单位问题:支持 px、pt、em、%,不支持rem、vw、vh实际开发中,建议:
使用简单的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许可证(免费)
开源项目可以使用 商业项目如果使用,必须将整个项目开源 不适合闭源商业软件 商业许可证(付费)
购买后可闭源使用 费用较高,按开发者数量收费 替代方案:
Apache PDFBox
Apache 2.0许可证,完全免费 功能较iText弱,HTML转PDF支持不好 适合基础PDF操作 OpenPDF
iText的分支,LGPL/MPL许可证 兼容iText 4.x API 商业项目可用,推荐替代方案 Flying Saucer(Open HTML to PDF)
HTML转PDF专用 LGPL许可证,商业可用 wkhtmltopdf
基于WebKit的命令行工具 通过Java调用系统命令 HTML渲染效果好,CSS支持完善 实际项目中,如果只是基础PDF生成,推荐OpenPDF;如果需要HTML转PDF,推荐Open HTML to PDF或wkhtmltopdf。
参考资源
-
iText官方文档 -
iText 5 API文档 -
Flying Saucer GitHub -
OpenPDF GitHub
📖 往期推荐
如果觉得有帮助,欢迎转发给需要的朋友 💙
有问题评论区见 ✨
夜雨聆风