ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

利用POI与docx4j实现Java后端Word转PDF的完整实践

利用POI与docx4j实现Java后端Word转PDF的完整实践 简介面向Java开发者的Word转PDF实用工具包解决Apache POI本身不支持直接输出PDF的痛点。资源包含两个可直接复用的工具类WordUtils、DocxUtils及配套的全部依赖JAR包覆盖POI、Docx4j、iText、xwpf-converter等关键组件省去逐个查找和版本匹配的麻烦。全包共15个文件其中12个JAR包提供Word解析、PDF生成与格式优化能力3个Java类封装了读取Word文档并转换为PDF的核心逻辑压缩包大小18.4MB轻量易部署。已有8386人学习使用。工具类支持.doc/.docx格式转换过程兼顾文本、段落与表格并包含字节流处理方式便于在内存中直接操作文件适合需要批量转换或集成到业务系统的开发者参考。 先说一个最容易被误解的点Apache POI 本身并不能把 Word 转成 PDF。上周我帮同事排查合同在线预览的问题代码里用XWPFDocument读入 docx折腾了半天表格样式最后想把文档输出成 PDF 才发现 POI 根本没有现成的toPdf方法。后来整条链路被我换成 POI 做内容预处理、docx4j 做 PDF 渲染表格宽度、中文显示这些问题才真正解决。这篇把两个可以直接拷贝的工具类和配套 jar 包版本都放出来Java 后端做办公文档转换的朋友应该用得上。如果你正在做合同预览、公文归档、报告导出这类功能大概率会遇到同一个需求把用户上传的 Word 转成 PDF 展示。网上资料很多但大多只给一句“用 docx4j 就行了”真到自己搭环境时依赖冲突、中文乱码、表格走样、内存溢出一个接一个。这篇文章就是一个完整的实操记录从 jar 包选型到工具类设计再到服务器部署的坑按顺序都讲清楚。1. 先掰正一个认知POI 是“翻译官”不是“画师”1.1 POI 到底能做什么、不能做什么POI 在 Java 世界里是处理 Office 文件的事实标准读 Excel、写 Word、解析 PPT 都靠它。但也正因为它太出名很多人默认它“什么都能干”。实际上 POI 的定位是读写 OOXML 和二进制 Office 格式它可以把 docx 解析成内存里的对象模型可以增删段落、改表格、插图片但它没有 PDF 渲染引擎。PDF 的排版规则和 Word 完全不一样。Word 是流式布局文字从左往右排碰到分页符才换页PDF 是物理页面布局每个字符的位置都要算出来字体度量、行高、字间距、分页都必须经过渲染引擎处理。POI 没有这一层能力所以你在XWPFDocument里翻遍所有方法也找不到一个exportPdf。打个比方POI 是把 Word 文档内容读出来、整理好的“翻译官”但它不会画画。真正把内容画到 PDF 页面上的是另一个库。1.2 Word 转 PDF 的三条常见路线Java 圈子做这件事主流方案就三个各有取舍。方案渲染引擎保真度部署成本适用场景docx4j纯 Java基于 JAXB 对象模型中等常规文档没问题低随应用一起部署中小型 Java 项目本文采用LibreOffice headlessLibreOffice 内核高接近 Word 渲染高服务器要装 Office 套件对最终效果要求极高的场景Aspose.Words商业引擎很高高授权费不便宜商业项目且预算充足如果你的服务器是干净的 Linux不想额外装软件docx4j 是最省事的。它不需要外部进程没有跨平台差异转换逻辑和业务代码跑在同一个 JVM 里出问题也容易排查。LibreOffice 方案保真度更高但每次转换都要拉起一个外部进程生命周期管理很麻烦转换一个文件要几秒批量跑的时候压力不小。1.3 为什么标题还是说“利用 POI 完成 Word 转 PDF”这里有一个很容易被忽略的细节docx4j 在加载 docx 做格式探测和结构解析时底层会用到 POI 的类。你去翻那些生产环境跑着的转换工程pom.xml里 docx4j 和 poi-ooxml 几乎总是成对出现。更实用的原因是转换之前往往要对文档做预处理。比如统一字体、清除空白段落、替换动态模板变量、修正页面大小。这些操作在 docx4j 里能做但 API 绕来绕去用 POI 的XWPFDocument写起来顺手得多。所以真实项目里的标准姿势是先用 POI 把 Word 内容“整理干净”再交给 docx4j 渲染 PDF。标题说“利用 POI 完成 Word 转 PDF”说的就是这个完整链路。2. 依赖清单与版本搭配劝你直接用 Maven别手动囤 jar2.1 Maven 坐标JDK 11 的现代组合如果你用的是 JDK 11 以上Spring Boot 项目我推荐 docx4j 11.x 配 POI 5.2.x。properties docx4j.version11.4.9/docx4j.version poi.version5.2.5/poi.version /properties dependencies dependency groupIdorg.docx4j/groupId artifactIddocx4j-core/artifactId version${docx4j.version}/version /dependency dependency groupIdorg.docx4j/groupId artifactIddocx4j-JAXB-ReferenceImpl/artifactId version${docx4j.version}/version /dependency dependency groupIdorg.apache.poi/groupId artifactIdpoi-ooxml/artifactId version${poi.version}/version /dependency /dependencies注意docx4j-JAXB-ReferenceImpl这个依赖JDK 11 以后 JAXB 从 JDK 里移除了docx4j 需要一个 JAXB 实现才能正常工作。漏掉它运行时会报ClassNotFoundException: javax.xml.bind.JAXBException。2.2 JDK 8 老项目的兼容组合如果你的项目还拴在 JDK 8 上别硬上 docx4j 11.x它要求 JDK 11 起步。老老实实用老搭配。properties docx4j.version8.3.9/docx4j.version poi.version4.1.2/poi.version /propertiesdocx4j 8.3.x 在 JDK 8 下非常稳定社区用的人多网上能搜到的踩坑记录也基本都是这个版本。POI 4.1.2 对 docx 的读写完全够用。如果你的公司在用 JDK 8不要犹豫直接选这套。2.3 没有 Maven 的 lib 部署最少需要哪些 jar有些老项目还是手动拷贝 jar 包到WEB-INF/lib这时候最烦的就是 docx4j 的传递依赖链比较宽。我整理了一份最小集合缺了哪个运行时都能看出来jar 包作用docx4j-core转换主库docx4j-JAXB-ReferenceImplJAXB 绑定poi、poi-ooxml、poi-ooxml-schemasOOXML 解析xmlbeansPOI schema 绑定commons-compress、commons-io压缩解压与文件操作guavadocx4j 用到的基础工具log4j-api、slf4j-api日志门面手动管理依赖时最容易漏的是xmlbeans。POI 解析 docx 时依赖它生成的对象模型漏掉后报错信息很隐晦直接NoClassDefFoundError很难定位到是缺这个 jar。2.4 版本冲突的经典症状NoSuchMethodError如果你的项目是 Spring Boot或者中间件里自带了poi一定要在依赖管理里统一版本。我遇到过最典型的情况系统里老业务用了 POI 3.17新模块为了转 PDF 引入 POI 5.2结果一运行docx4j 调用 POI 的某个新方法直接NoSuchMethodError。排查了半个多小时最后发现是poi-ooxml-schemas版本被旧依赖覆盖了。解法很简单在dependencyManagement里把 POI 版本统一写死所有模块都用同一个版本。docx4j 同理docx4j-core和docx4j-JAXB-ReferenceImpl必须同一个版本不能一个 8.3 一个 11.4否则NoSuchMethodError教你做人。3. 核心转换工具类POI 清洗内容docx4j 负责最后一公里3.1 为什么转换前要先走一遍 POI直接拿 docx4j 加载原始文件也能转但生产环境里我强烈建议在前面加一层 POI 预处理。原因有三个一是统一字体。docx4j 渲染 PDF 时如果文档里指定的字体系统里没有它会自动回退中文很容易变成方块或乱码。先遍历一遍段落把字体统一成“宋体”或“微软雅黑”能省掉大部分字体问题。二是清理空白段落。Word 文档里经常残留大量空行这些空行到了 PDF 里就是大片空白区域页数也莫名膨胀。POI 遍历起来很轻量把连续空行处理掉PDF 版式立刻干净。三是修正页面尺寸。POI 创建出来的 docx 如果没有明确设置pgSzdocx4j 可能默认用 Letter 而不是 A4转换出的 PDF 比例看起来就是不对。在预处理阶段把 A4 尺寸写死后面就不会出幺蛾子。3.2 Word2PdfUtils 完整代码这个工具类只做一件事把单个 docx 文件转成 PDF兼容流输入默认不启用 POI 清洗稳定优先。import org.apache.poi.util.Units; import org.apache.poi.xwpf.usermodel.XWPFDocument; import org.docx4j.Docx4J; import org.docx4j.convert.out.pdf.PdfConversion; import org.docx4j.openpackaging.exceptions.Docx4JException; import org.docx4j.openpackaging.packages.WordprocessingMLPackage; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTSectPr; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; public class Word2PdfUtils { private Word2PdfUtils() {} /** * POI 预处理开关。模板文档可控、样式简单时才建议打开。 * 复杂文档含嵌入对象、域代码预处理可能导致内容丢失默认关闭。 */ private static final boolean ENABLE_POI_CLEAN false; public static void convertDocxToPdf(File srcDocx, File targetPdf) throws IOException { if (srcDocx null || !srcDocx.exists() || !srcDocx.getName().toLowerCase().endsWith(.docx)) { throw new IllegalArgumentException(源文件必须是已存在的 docx 文件); } if (targetPdf null) { throw new IllegalArgumentException(目标 PDF 文件不能为空); } File parent targetPdf.getParentFile(); if (parent ! null !parent.exists()) { parent.mkdirs(); } // 1. 可选先用 POI 做内容清洗 File source srcDocx; File cleaned null; if (ENABLE_POI_CLEAN) { cleaned preProcessByPoi(srcDocx); if (cleaned ! null) { source cleaned; } } // 2. POI 的部分工作已经完成最后交给 docx4j 渲染 PDF try (FileInputStream fis new FileInputStream(source); FileOutputStream fos new FileOutputStream(targetPdf)) { WordprocessingMLPackage mlPackage Docx4J.load(fis); Docx4J.toPDF(mlPackage, fos); } catch (Docx4JException e) { throw new IOException(docx 转 pdf 失败: e.getMessage(), e); } finally { if (cleaned ! null cleaned ! srcDocx) { cleaned.delete(); } } } public static void convertDocxToPdf(InputStream in, OutputStream out) throws IOException { File tempDocx File.createTempFile(word2pdf-, .docx); File tempPdf File.createTempFile(word2pdf-, .pdf); try (FileOutputStream fos new FileOutputStream(tempDocx)) { byte[] buffer new byte[8192]; int len; while ((len in.read(buffer)) ! -1) { fos.write(buffer, 0, len); } } try { convertDocxToPdf(tempDocx, tempPdf); try (FileInputStream fis new FileInputStream(tempPdf)) { byte[] buffer new byte[8192]; int len; while ((len fis.read(buffer)) ! -1) { out.write(buffer, 0, len); } } } finally { tempDocx.delete(); tempPdf.delete(); } } /** * POI 清洗清空行、设置 A4 页面。 * 此处只做最安全的操作实际项目可按需扩展为字体统一、模板变量替换等。 */ private static File preProcessByPoi(File srcDocx) throws IOException { File temp new File(srcDocx.getParentFile(), srcDocx.getName().replace(.docx, -cleaned.docx)); try (XWPFDocument document new XWPFDocument(new FileInputStream(srcDocx)); FileOutputStream out new FileOutputStream(temp)) { for (org.apache.poi.xwpf.usermodel.XWPFParagraph paragraph : document.getParagraphs()) { if (paragraph.getText().trim().isEmpty()) { paragraph.getCTP().setPPr(null); } } CTSectPr sectPr document.getDocument().getBody().getSectPr(); if (sectPr.getPgSz() null) { sectPr.addNewPgSz(); } // A4 尺寸单位是 DXA宽 11906高 16838 sectPr.getPgSz().setWidth(11906); sectPr.getPgSz().setHeight(16838); document.write(out); return temp; } catch (Exception e) { // 预处理失败不阻塞转换回退到原始文件 temp.delete(); return null; } } }3.3 关键点拆解先说参数设计我对外只暴露了 File 和 InputStream 两种重载。优先推荐传 File因为Docx4J.load接收 InputStream 时不会做文件扩展名探测格式判断全靠文件头某些不规范的 docx 文件可能导致加载失败。传 File 的话docx4j 会根据扩展名和内容双重校验更稳。再说那个ENABLE_POI_CLEAN开关。为什么默认关掉因为 POI 把文档重新写一遍某些复杂文档可能丢失域代码、嵌入对象这类高级特性。我自己的习惯是模板文档是我们程序自己生成的开关打开用户上传的不可控文档开关保持关闭。安全第一。Docx4J.toPDF(mlPackage, fos)是核心调用。docx4j 的 PDF 转换器会遍历 WordprocessingMLPackage 里的段落、表格、图片结合 sectPr 里的页面定义渲染成 PDF 输出流。注意这个方法不会自己关闭输出流所以我用 try-with-resources 管理。关于字体发现PhysicalFonts.discoverPhysicalFonts()在本文还有配套的精品资源点击获取
返回列表