简介:面向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 headless | LibreOffice 内核 | 高,接近 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,别手动囤 jar
2.1 Maven 坐标:JDK 11+ 的现代组合
如果你用的是 JDK 11 以上,Spring Boot 项目,我推荐 docx4j 11.x 配 POI 5.2.x。
<properties> <docx4j.version>11.4.9</docx4j.version> <poi.version>5.2.5</poi.version> </properties> <dependencies> <dependency> <groupId>org.docx4j</groupId> <artifactId>docx4j-core</artifactId> <version>${docx4j.version}</version> </dependency> <dependency> <groupId>org.docx4j</groupId> <artifactId>docx4j-JAXB-ReferenceImpl</artifactId> <version>${docx4j.version}</version> </dependency> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-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.version>8.3.9</docx4j.version> <poi.version>4.1.2</poi.version> </properties>docx4j 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-ReferenceImpl | JAXB 绑定 |
| poi、poi-ooxml、poi-ooxml-schemas | OOXML 解析 |
| xmlbeans | POI schema 绑定 |
| commons-compress、commons-io | 压缩解压与文件操作 |
| guava | docx4j 用到的基础工具 |
| 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 如果没有明确设置pgSz,docx4j 可能默认用 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()在
本文还有配套的精品资源,点击获取