docx4j实现HTML转Word:选型对比、环境配置与避坑指南
2026/9/16 22:37:14 网站建设 项目流程

1. 为什么选 docx4j:HTML 转 Word 的方案对比与选型逻辑

先交代一下我遇到的实际场景。业务线每天会生成几十份 HTML 报表,页面里带完整的目录、表格、页眉页脚样式,领导要求能一键导出 Word 用于盖章和归档。这个需求听起来不复杂,但真正落地的时候会发现,“HTML 转 Word” 从来都不是用一个第三方库调一个方法就能完美收工的事。HTML 的标签语义和 Word 的 OOXML 文档模型完全是两套体系,中间要过的坎包括样式映射、表格栅格、图片资源、字体回退,甚至还有编码问题。

我当时的第一反应是 Apache POI。POI 是 Java 操作 Office 的老牌库,社区大、资料多。但真正开始写就发现不对劲:XWPF 的设计思路是让你从零开始构建 Word 文档,一个段落要手动创建 XWPFRun,一个表格要手动创建 XWPFTable,然后挨个设置单元格的 tcPr、borders 这些底层属性。如果你的内容来源是后台富文本编辑器输出的 HTML,你需要先把 HTML 解析成结构体,再把这些结构体翻译成 XWPF 的各个对象,这个翻译层的工程量非常大。更别说 HTML 里那些内联样式、嵌套列表、表格合并单元格,用 POI 手工映射很容易写出几百行逻辑只为了处理一个表格。

后来也评估过 Aspose.Words。转换效果确实好,但商业授权费用对一个内部工具来说有点肉疼,而且我这边需要高度可控的服务端批处理能力,不希望把核心逻辑绑死在商业黑盒里。兜了一圈,最后选定了 docx4j + docx4j-ImportXHTML 这条路线。

docx4j 的核心模型是基于 JAXB 的 OOXML 映射,WordprocessingMLPackage 直接对应 docx 包的根,MainDocumentPart 对应 document.xml。它的优势不在于提供一个“HTML 转 Word”的魔法方法,而在于它把 Word 文档变成了一个你可以直接操作和检查的 Java 对象树。配合 docx4j-ImportXHTML 模块,它会在服务端把 XHTML 解析成真正的 w:p、w:r、w:tbl 这些 OOXML 元素,而不是把 HTML 塞进文档等 Word 自己解析。

这是我很看重的一点。有人可能觉得,Word 自己也能打开 HTML 文件,但那种方式依赖客户端行为,格式还原程度不可控,而且 Word 处理非标 HTML 时很容易弹出“文件格式与扩展名不匹配”的提示。docx4j 的做法是从源头上把 HTML 翻译成原生 Word 内容,生成的结果就是一个标准的、干净的 docx。

从维护角度看,docx4j 还有个好处:它可以作为模板引擎使用。先用 Word 设计一个带占位符的模板,然后用 docx4j 打开模板、在指定位置插入转换后的内容。这个能力对报表系统来说太重要了,因为业务场景里通常不是从零生成 Word,而是套用固定封面、固定页眉页脚、固定签章位置,再把动态内容填充进去。

下表是我当时整理的几个方案对比,分享给同样在做选型的朋友参考。

方案优势需要警惕的坑
POI XWPF库轻量,API 成熟,可精细控制所有内容都得手工构建,HTML 到 Word 的翻译层等于自己写一遍
Aspose.Words转换质量高,支持格式多商业授权,价格高,批量部署时要严格合规
Word AltChunkWord 打开时自动渲染 HTML结果依赖 Word 客户端,服务端不可控,易触发安全提示
docx4j + ImportXHTML开源可控,转换结果是原生 OOXML 元素学习曲线稍陡,版本选型要仔细

最终我选择了 docx4j,这也是这篇文章后面所有讨论的基础。

2. 环境准备:版本选型与 Maven 配置

选好技术方向后,第一件事是搭环境。docx4j 这个库的版本变化比较大,8.x 和 11.x 的接口签名、模块划分都很不一样。如果你是从别人的老项目里抄依赖,很容易踩进“类找不到”的坑。

我用的组合是 docx4j-JAXB-ReferenceImpl + docx4j-ImportXHTML,版本选 11.4.9。之所以选 ReferenceImpl,是因为它使用独立的 JAXB 实现,不依赖 JDK 内置的 JAXB,在 Java 11 以及更高版本上跑起来更省心。如果你还在用 Java 8,用 InternalImpl 也能跑,但为了少一点环境差异,建议直接上 ReferenceImpl。

pom.xml 里的依赖配置如下:

<properties> <docx4j.version>11.4.9</docx4j.version> </properties> <dependencies> <dependency> <groupId>org.docx4j</groupId> <artifactId>docx4j-JAXB-ReferenceImpl</artifactId> <version>${docx4j.version}</version> </dependency> <dependency> <groupId>org.docx4j</groupId> <artifactId>docx4j-ImportXHTML</artifactId> <version>${docx4j.version}</version> </dependency> </dependencies>

这里有个容易踩的坑:网上很多老教程写的是docx4j这个 artifactId,但在 11.x 版本里,主库已经被拆成了docx4j-JAXB-ReferenceImpldocx4j-JAXB-InternalImpl两个方向,如果直接引老坐标,依赖解析会出问题,或者拉到的是一个很老的版本。所以你要是新项目,认准我上面这个组合就行。

docx4j-JAXB-ReferenceImpl 会传递依赖一批 JAXB 相关的库,比如 jakarta.xml.bind-api、jaxb-runtime 这类,Maven 会自动处理,不需要你手动加。但有一点要注意:如果你的项目里已经有别的 JAXB 实现,比如 GlassFish Jersey 或者老版的 JAXB RI,建议把传递依赖排一下,避免运行时出现 Multiple JAXB contexts 之类的冲突。

另外,Java 版本建议用 8 以上,我实际测试 11 和 17 都没问题。如果你的服务运行在容器里,注意给 JVM 留足内存,因为 docx4j 在构建对象树时比较吃内存,尤其是 HTML 里嵌套了大量表格时。后面我会在性能部分细说。

依赖搞定后,先别急着写业务代码。我建议先写一个最简 main 方法,创建一个空的 WordprocessingMLPackage,然后保存成一个 docx,验证环境没有问题。这一步能帮你把“依赖缺失”和“业务代码 bug”这两个问题隔离开,排查起来舒服很多。

3. 核心实现:从 HTML 到 Word 的转换流程

3.1 先跑通一段最小可用代码

先看一段能跑通全流程的 Java 代码。这段代码把一个 XHTML 字符串转换成 Word 文档,保存成 output.docx。

import org.docx4j.Docx4J; import org.docx4j.convert.in.xhtml.XHTMLImporterImpl; import org.docx4j.openpackaging.packages.WordprocessingMLPackage; import org.docx4j.openpackaging.parts.WordprocessingML.NumberingDefinitionsPart; import java.io.File; import java.io.FileOutputStream; import java.util.List; public class HtmlToWordDemo { public static void main(String[] args) throws Exception { // 1. 创建空的 Word 文档包 WordprocessingMLPackage wordMLPackage = WordprocessingMLPackage.createPackage(); // 2. 初始化默认编号定义,处理 ul/ol 列表必须有这一步 NumberingDefinitionsPart ndp = new NumberingDefinitionsPart(); ndp.unmarshalDefaultNumbering(); wordMLPackage.getMainDocumentPart().addTargetPart(ndp); // 3. 初始化 XHTML 导入器 XHTMLImporterImpl importer = new XHTMLImporterImpl(wordMLPackage); importer.setHyperlinkStyle("Hyperlink"); // 4. 设置 baseURL,图片和外部 CSS 会基于它解析 String baseUrl = new File("src/main/resources").toURI().toURL().toString(); // 5. 准备 XHTML 内容 String xhtml = """ <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"/> <style> body { font-family: "Microsoft YaHei", sans-serif; font-size: 10.5pt; } h1 { font-size: 22pt; color: #333; } table { width: 100%; border-collapse: collapse; } th, td { border: 1px solid #999; padding: 6px; } </style> </head> <body> <h1>自动化报告</h1> <p>这是正文内容,支持<b>加粗</b>、<i>斜体</i>、<u>下划线</u>。</p> <ul> <li>第一项</li> <li>第二项</li> </ul> <table> <tr><th>字段</th><th>说明</th></tr> <tr><td>版本</td><td>1.0</td></tr> </table> </body> </html> """; // 6. 转换:返回的是 OOXML 内容对象列表 List<Object> content = importer.convert(xhtml, baseUrl); // 7. 把转换结果追加到主文档 wordMLPackage.getMainDocumentPart().getContent().addAll(content); // 8. 保存 try (FileOutputStream out = new FileOutputStream(new File("output.docx"))) { Docx4J.save(wordMLPackage, out); } } }

这个流程看着简单,但里面有几个必须注意的细节。

第一个是 NumberingDefinitionsPart。docx4j 默认创建的空文档里没有编号定义,如果不手动初始化,HTML 里的<ul><ol>虽然也能转换,但生成的编号是坏的,Word 里打开列表项会被渲染成普通段落,或者编号从错误的位置开始。unmarshalDefaultNumbering()会把 docx4j 内置的一套默认编号模板导入进来,这样<ul><ol>才能正常映射成 w:numPr。

第二个是 baseURL。XHTML 里的 CSS 外部样式表和图片引用,都是基于这个路径去解析的。如果你不设置,图片和样式会找不到。我通常会把 baseURL 设置成项目里的一个静态资源目录,或者根据业务情况动态传入 HTML 对应的服务端路径。

第三个是 importer.convert() 的返回值。它返回的不是一个新的 WordprocessingMLPackage,而是一个 List,里面装的是 XHTML 元素转换后的 OOXML 内容对象,比如 w:p、w:tbl、w:sdt 这些。你可以把这个 List 直接塞进原有文档的 MainDocumentPart.getContent() 里,实现“在已有文档中插入 HTML 内容”的效果。这是 docx4j-ImportXHTML 最实用的地方。

3.2 先做一步 XHTML 清洗,能省很多事

HTML 和 XHTML 的区别没你想的那么小。后台编辑器输出的 HTML 是宽松模式的,标签不闭合、属性不带引号、<br>不写自闭合,这种情况在浏览器里没问题,但 XHTMLImporter 是基于 XML 解析的,遇到不合规的 HTML 会直接报解析异常,或者静默丢内容。所以转换之前,我建议先做一步 XHTML 清洗。

我通常用 jsoup 做这件事。它本身就是 HTML 解析库,能把不规范的 HTML 纠正成 XML 规范的 XHTML。

import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Entities; public String cleanHtmlToXhtml(String rawHtml) { Document doc = Jsoup.parse(rawHtml); doc.outputSettings() .syntax(Document.OutputSettings.Syntax.xml) .escapeMode(Entities.EscapeMode.xhtml) .charset("UTF-8"); return doc.outerHtml(); }

这一步的作用是把 src 属性、空标签、布尔属性这些全部规范成 XHTML 能接受的形式。比如<meta charset="utf-8">会被补成<meta charset="utf-8"/><br>会变成<br/>。做完这步再交给 XHTMLImporter,可以避免很多“莫名其妙丢样式”的问题。

3.3 把内容插入已有 Word 模板

前面说的是从空白文档开始生成,但真实业务里更常见的是套模板。比如固定封面、固定落款、固定页眉页脚,只在文档中间插入动态内容。docx4j 处理这种场景非常顺手。

WordprocessingMLPackage wordMLPackage = WordprocessingMLPackage.load(new File("template.docx")); XHTMLImporterImpl importer = new XHTMLImporterImpl(wordMLPackage); importer.setHyperlinkStyle("Hyperlink"); List<Object> content = importer.convert(xhtml, baseUrl); // 追加到文档末尾 wordMLPackage.getMainDocumentPart().getContent().addAll(content); try (FileOutputStream out = new FileOutputStream(new File("result.docx"))) { Docx4J.save(wordMLPackage, out); }

如果你不想插到末尾,而是插到某个占位段落后面,可以先遍历wordMLPackage.getMainDocumentPart().getContent(),找到锚点段落的下标,然后用add(index, obj)插入。这个方法在做自动化报表时非常实用,比如先用 Word 设计好封面页、声明页、落款页,然后在中间预留一个空段落,代码里找到这个段落的 index,把转换出的 List 插进去,最后另存为新文档。整个过程完全是服务端自动化,不需要人工再打开 Word 去调整。

4. 样式、表格、图片三大高频场景的处理细节

4.1 CSS 样式映射到 Word 属性时,别拿 Word 当浏览器

docx4j-ImportXHTML 内置了一个 CSS 解析器,能把常见的内联样式和<style>样式映射到 Word 的 run 属性上。映射关系大致如下:

CSS 样式映射后的 Word 属性说明
font-familyw:rFonts中文字体建议显式写成 Microsoft YaHei 或 SimSun
font-sizew:sz注意 pt 与 px 的换算,浏览器默认 16px 约等于 12pt
font-weight: boldw:b
font-style: italicw:i
colorw:color
background-colorw:shd
text-decoration: underlinew:u
text-alignw:jc
borderw:tblBorders / w:tcBorders表格边框支持得比较好
line-heightw:spacing

需要特别提醒的是,CSS 里那些布局控制属性,比如 flex、grid、position: absolute,docx4j 是不会处理的。Word 的排版模型是流式的,XHTMLImporter 能做的只是把文本语义、段落间距、字体层级映射过去。你在设计 HTML 模板时就要有意识地限制复杂度,别指望 Word 帮你做响应式布局。

我在实际项目中遇到过这种情况:HTML 里用了大量 margin 和 padding 来控制元素间距,转换后 Word 里的段落间距完全不对。后来把 HTML 模板里的样式全部改成 Word 友好的写法,比如用空段落控制间距、用表格控制横向布局,最终效果才稳定下来。

4.2 表格列宽为什么拖不动,怎么让它能拖

搜过“word 表格列宽无法拖动”的朋友应该明白,这个坑不是只有 docx4j 会踩,任何 POI、docx4j 生成的表格都会遇到。问题的根源在于 Word 对表格列宽的控制,靠的是 tblGrid 里的 gridCol 和每个单元格 tcW 的宽度值。如果表格没有 gridCol,或者宽度值是 auto,Word 打开后会进入自动调整模式,用户拖表格线时列宽不会按预期变化,甚至根本拖不动。

要解决这个问题,推荐在 XHTML 里把表格布局显式设为 fixed,并通过 colgroup 定义列宽。

<table style="width:100%; table-layout:fixed; border-collapse:collapse;"> <colgroup> <col style="width:25%"/> <col style="width:75%"/> </colgroup> <tbody> <tr> <th>名称</th> <th>说明</th> </tr> <tr> <td>warp</td> <td>固定布局测试</td> </tr> </tbody> </table>

转换后,docx4j 会在 OOXML 里生成对应的 tblGrid 和固定布局属性。用 Word 打开时,表格属性里的“列宽”显示为具体值,拖动表格线也能正常调整。

如果转换后还是拖不动,还有一个可能:Word 里表格属性设置了“自动调整”,把它改成“固定列宽”就行。实操时可以在 Word 里右键表格 → 表格属性 → 选项 → 取消勾选“自动重调尺寸以适应内容”。

这里顺带说一句,如果你在网上去搜“poi 设置 word 表格单元格宽度”,会发现 POI 方案绕来绕去很繁琐。docx4j 至少提供了一个更符合直觉的路径:在 XHTML 里定义好 colgroup,转换时自动映射成 tblGrid,省去手动操作底层对象。

4.3 图片与 baseURL:先解决资源路径,再谈样式

图片是 HTML 转 Word 里最容易出问题的一环,核心还是 baseURL。.convert(xhtml, baseUrl)里的 baseUrl 决定了<img src="images/pic.png">这种相对路径图片去哪里找。如果 baseURL 设置成file:///D:/temp/,那么实际加载的就是D:/temp/images/pic.png

如果业务系统传过来的 HTML 里是相对路径的图片,而服务端又没法直接暴露静态目录,可以先在代码里把图片下载到本地临时目录,再把 img 标签的 src 改成新路径,最后设置 baseURL 为这个临时目录。这招在处理“html 邮件转 Word”场景时特别管用,因为邮件正文里的图片经常是 CID 引用,需要额外替换。

另一个需要注意的点是图片体积。有些富文本编辑器会把大图直接 base64 内嵌到 HTML 里,一个 src 动辄几百 KB。docx4j 转换时会把这些图片解出来放到 word/media 目录,如果图片太多,生成的 docx 会非常大,而且转换时内存占用也会显著上升。建议在进入转换链路之前,先对 base64 图片做一次压缩或者降采样。

4.4 中文字体:别指望系统帮你猜

HTML 页面在浏览器里显示正常,到了 Word 里变成宋体,原因通常在 font-family。XHTMLImporter 会把 CSS 的 font-family 映射到 Word 的 w:rFonts,但中文字体的定义方式有讲究。推荐在 CSS 里显式声明中文字体名:

body { font-family: "Microsoft YaHei", "微软雅黑", sans-serif; }

这样转换后 Word 的 west font 和 eastAsia font 才会正确设置。如果完全不做中文字体相关配置,docx4j 会按默认字号和默认字体处理,最终在 Word 里显示成宋体或者默认主题字体。

还要注意一点:字体能否最终正常显示,取决于打开 docx 的那台机器是否安装了对应字体。你就算在 CSS 里写了 Microsoft YaHei,对方机器上没有这个字体,Word 照样会回退。要做导出工具的同学,最好在需求阶段就跟业务方确认清楚目标文件会在什么环境被打开。

5. 实际踩坑:常见问题与排查清单

以下是我在这套方案上踩过的坑,整理成一张速查表,方便大家排查。

问题现象大概率原因处理办法
Word 表格列宽无法拖动表格布局是 autofit,或 tblGrid 没有正确生成在 XHTML 中使用 table-layout:fixed + colgroup 定义列宽
生成的 Word 中列表全变成普通段落缺少 NumberingDefinitionsPart手动调用 unmarshalDefaultNumbering() 并 addTargetPart
Word 打开提示文档内容无法读取HTML 不是合法 XHTML,或残留 AltChunk用 jsoup 清洗 HTML;避免 AltChunk 方式
Word 关闭时卡顿文档内含未处理的 AltChunk,或大量外部资源引用统一走 XHTMLImporter 生成原生内容,移除 AltChunk
保存显示磁盘已满临时目录空间不足、输出目录权限问题、图片资源过多检查 java.io.tmpdir 和输出目录;压缩图片
中文乱码编码不一致统一 UTF-8,Html 里带上 charset,代码里用 getBytes(StandardCharsets.UTF_8)
中文字体被替换成宋体CSS 未显式声明 eastAsia 字体设置 font-family 为 Microsoft YaHei 等中文字体

单独展开几个重点:

5.1 为什么 Word 关闭时很卡

根源基本都出在 AltChunk 上。如果你用addAltChunk(AltChunkType.Xhtml, bytes)的方式把 HTML 原样塞进 docx,Word 打开文档后需要用自己的解析引擎把这段 HTML 再转一遍。HTML 越复杂,Word 就越卡,尤其在保存和关闭的时候,它还要重新计算一遍格式。再加上 Word 对非标 HTML 的容错度很低,很容易弹修复窗口或者直接崩。

我的做法是,所有需要转换的 HTML 都走 XHTMLImporter 处理成原生内容,绝对不在最终文档里保留 AltChunk。这样生成的 docx 里没有需要 Word 二次解析的内容,打开和关闭都很干净。

5.2 Word 保存显示磁盘已满,但不一定是磁盘真的满了

报“磁盘已满”的时候,多数人第一反应是看磁盘剩余空间,但其实很多时候问题出在临时目录或权限上。docx 本质是 zip 打包,docx4j 在转换和保存过程中会使用大量临时文件,尤其是当 HTML 里嵌入了很多图片时。如果java.io.tmpdir指向的目录空间不足或不可写,Word 在保存时就会误报磁盘已满。

遇到这个问题,先把 JVM 的临时目录换到一个有空间、可写的路径,比如-Djava.io.tmpdir=/data/tmp,同时确认输出目录有写权限。如果是在 Windows 上跑,还要留意杀毒软件是不是锁住了文件。

5.3 列表编号异常,大多数情况是忘了一步

很多第一次用 docx4j-ImportXHTML 的朋友会遇到列表转出来没有编号的问题。这个我已经在前面强调过了,就是缺少 NumberingDefinitionsPart。docx4j 的空文档里不带编号定义,<ul><ol>样式会丢失,所以创建包之后一定要执行:

NumberingDefinitionsPart ndp = new NumberingDefinitionsPart(); ndp.unmarshalDefaultNumbering(); wordMLPackage.getMainDocumentPart().addTargetPart(ndp);

这行代码不复杂,但很容易被忽略,尤其在你复制了网上老版本代码的时候,有些版本的处理方式不太一样。

6. 扩展经验:批量自动化与后续演进建议

6.1 批量转换时的线程与内存控制

如果你不是单文档转换,而是要做一个服务接口,同时处理大量请求,有几个点要注意。

docx4j 的 WordprocessingMLPackage 不是线程安全的,每个转换任务都应该有自己独立的包实例,不能把同一个包放在多线程里并发操作。正确的做法是每个任务内部创建 WordprocessingMLPackage、创建 XHTMLImporterImpl、转换、保存,各自独立,互不干扰。

内存方面,docx4j 构建对象树时比较吃内存,尤其遇到大表格、大图片时会更明显。我在一个批处理任务里同时跑 50 个文档的转换,堆内存设了-Xmx2g都出现过 OOM,后来把任务改成固定大小的线程池,逐个消费,内存才稳定下来。如果你的服务也是分批处理,记住一点:转换完一个就释放一个,不要让中间对象堆积。

另外,如果图片来自网络 URL,docx4j 在拉取图片时没有做超时控制的话,很容易因为某个图片响应慢拖垮整个任务。我建议在进入 docx4j 之前,先把远程图片下载到本地,替换 src 路径,再交给转换器。虽然多一步代码,但稳定性提升非常明显。

6.2 Markdown、HTML 邮件等场景怎么复用这套链路

这套 docx4j + ImportXHTML 的方案,只接受 HTML,不接受 Markdown。但 Markdown 转 Word 的常见做法是先转成 HTML,再走这套链路。社区里有很多 markdown 转 HTML 的工具,例如 commonmark-java、flexmark,先转成 XHTML,再用同样的 XHTMLImporter 导入 Word,整个过程很顺滑。

HTML 邮件也是类似。很多企业内部的周报、通知都是 HTML 邮件正文,格式相对简单,没有复杂 JS 和动态布局,非常适合直接转换。只要把邮件正文的 HTML 清洗成 XHTML,再用上面的代码处理即可。

PDF 转 Word 就不建议硬套这套方案了。PDF 是排版后的固定布局,和 HTML 的流式模型不一样,docx4j 没法直接解析 PDF。如果业务上确实有 PDF 转 Word 的需求,可能需要单独选型渲染或 OCR 方案。

6.3 公式、复杂表格和后续维护

如果 HTML 里带了 MathJax 或 MathML 公式,docx4j-ImportXHTML 目前不能完美转换成 Word 的原生公式(OMML)。我在项目里处理这个问题的办法是,把公式先渲染成高清图片,再替换到 HTML 里。虽然不能编辑,但至少保证了视觉呈现一致。

关于后续维护,我有两个习惯建议。第一是锁定版本,docx4j 的 API 在不同大版本间变化很大,升级前一定要看迁移说明。第二是建立回归测试集,把常见的 HTML 片段(标题、列表、表格、图片、复杂样式)存成测试用例,每次升级依赖后跑一遍,确认生成的 docx 还能用 LibreOffice 命令行转换成 PDF 做对比检查,避免某个小版本更新引入隐藏问题。

我现在还会在这个方案上继续做一些小改造,比如用模板变量占位的方式把封面和正文分开处理,再把转换后的 HTML 内容插入到指定书签后面,这比直接从头生成文档要稳定很多。如果你正在做类似的 HTML 转 Word 服务,希望这篇整理能让你少走一点弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询