简介:基于 docx4j 与 docx4j-ImportXHTML 的 Java 工程源码包,面向需要将 HTML 转换为 Word/PDF 的开发人员,用于解决办公自动化中批量生成文档、内容复用与格式兼容等实际问题。压缩包共 170 个文件,包含 10 个 Java 源码、10 个已编译的 class 文件、132 个 XML 配置与结构描述、8 个 HTML 模板和 3 个字体文件,整体大小 16.71MB,目录结构清晰,便于按模块检索。目前已汇集 761 人次学习/下载,适合具有一定 Java 基础、正在搭建文档转换模块的技术人员参考。资源给出了从 HTML 模板准备、占位符动态替换到 docx4j-ImportXHTML 解析生成 Word 的完整链路,并带有 PDF 导出扩展示例。通过阅读核心实现类,可掌握段落、列表、表格等元素的转换规则,并可借鉴项目结构快速嵌入到自己的 Spring Boot 工程中,有效缩短开发调试周期。
1. 把 HTML 转成 Word:docx4j + ImportXHTML 这条线为什么值得走
做后端或者搞自动化办公的兄弟,大概率都撞过“HTML 转 Word”这堵墙。用 HtmlUnit 截图?出来的图片没法编辑。用 POI 硬拼段落?写两页代码手腕先酸了。我最常被问到的方案是 openoffice 转 PDF 再转 Word,那叫一个难用。这两年我用得最顺、翻车最少的一条路,是 docx4j + docx4j-ImportXHTML。这组合能干的事很直接:把你手头还算干净的 XHTML,转成一个能被 Word 正常打开、文字可选、样式保留的 .docx 文件。适合你在做 CMS 导出、工单打印、报告批量生成这类场景,不想让前端同事介入,后端一条命令把 HTML 文本变成 Word 文档。下面我把依赖怎么配、核心 API 怎么调、图片和表格在哪个环节最容易炸,以及我踩过的那些坑一次说清。
2. 环境与依赖:先把 docx4j 和 ImportXHTML 版本绑定理顺
2.1 Maven 依赖与版本玄学
docx4j 这块的版本坑相当邪门。早期 3.x 时代,docx4j-ImportXHTML 和主包还能凑合分开引,到 8.x 之后主包自己就把 ImportXHTML 的能力收了。如果你照着网上 2015 年那种老帖子抄,几乎必翻车。我现在固定用这条组合,稳定跑了快一年:
<dependency> <groupId>org.docx4j</groupId> <artifactId>docx4j-JAXB-ReferenceImpl</artifactId> <version>8.3.9</version> </dependency> <dependency> <groupId>org.docx4j</groupId> <artifactId>docx4j-ImportXHTML</artifactId> <version>8.3.9</version> </dependency>逻辑说明:docx4j-ImportXHTML这个模块负责把 XHTML 文档解析成 docx4j 的内部对象树(主要是WordprocessingMLPackage),它依赖docx4j-core或docx4j-JAXB-ReferenceImpl来构建那个包结构。这里要敲黑板:版本号必须跟主包完全一致,差一个小版本都可能触发NoSuchMethodError,因为内部类名在迭代时改过。参数说明:如果你的项目用的是 Java 8,别碰 11.x 以上的 docx4j,它要求 JDK 11+,Java 8 最后一版能用的是 8.3.x 这条线。
2.2 先写二十行代码跑通全链路
依赖归位后,先别急着接业务逻辑,我习惯写一个最小可跑的测试,把“内存字符串转 docx 文件”这条路先趟平。这步看着简单,但能过滤掉八成环境问题。
import org.docx4j.openpackaging.packages.WordprocessingMLPackage; import org.docx4j.openpackaging.parts.WordprocessingML.MainDocumentPart; import org.docx4j.convert.xhtml.XHTMLImporter; import org.docx4j.convert.xhtml.XHTMLImporterImpl; import java.io.FileOutputStream; public class QuickStart { public static void main(String[] args) throws Exception { String xhtml = "<html><head><title>测试</title>" + "<style>body { font-family: SimSun; font-size: 14pt; }</style>" + "</head><body><h1>标题一</h1><p>正文段落</p></body></html>"; WordprocessingMLPackage wordMLPackage = WordprocessingMLPackage.createPackage(); XHTMLImporter importer = new XHTMLImporterImpl(wordMLPackage); wordMLPackage.getMainDocumentPart().convert(xhtml, importer); wordMLPackage.save(new FileOutputStream("output.docx")); System.out.println("转换完成"); } }逻辑说明:createPackage()先建一个空白的 Word 文档骨架,XHTMLImporterImpl拿到这个包的引用后,会把字符串形式的 XHTML 转换成MainDocumentPart里的一系列段落和表格对象。最后一行的save直接落盘成 .docx 文件。参数说明:convert方法有两个重载,传字符串还是传InputStream都合法,但字符串里如果含超大的 base64 图片,强烈建议改用流式,免得内存被撑爆。另外,我这里在style里写的font-family是给 Word 指定中文字体,这个后面还要细说。
3. 核心转换 API 与样式处理:把 XHTML 里的 CSS 映射到 Word 格式
3.1 能用和不能用的 CSS 边界
很多人第一次用都以为 CSS 能全覆盖,实际上 docx4j-ImportXHTML 走的是org.docx4j.convert.xhtml这条解析链,它对 CSS 的支持有限定范围。font-family、font-size、color、background、text-align、margin、padding这些基础属性映射得不错;但是float、position、flex、grid这类布局属性,它直接选择忽略。我一般这么跟产品说:这工具负责把“文档语义”转过去,不负责把“网页布局”搬过去。
3.2 通过 XHTMLImporter 设置页面与样式映射
页面尺寸、页边距这些不能靠 CSS 硬写,得通过XHTMLImporterImpl里的PageSetup相关的方法来调。常见的做法是,在转换前先拿到包对象,然后设置页面数据。
import org.docx4j.convert.xhtml.XHTMLImporterImpl; import org.docx4j.openpackaging.packages.WordprocessingMLPackage; import org.docx4j.wml.PageSz; import org.docx4j.wml.SectPr; WordprocessingMLPackage wordMLPackage = WordprocessingMLPackage.createPackage(); XHTMLImporterImpl importer = new XHTMLImporterImpl(wordMLPackage); importer.setPageSize(PageSz.A4); importer.setPageMargins(1440, 1440, 1440, 1440); // 单位是 twip importer.setToWordDefaultTableStyle(true); wordMLPackage.getMainDocumentPart().convert(xhtmlString, importer);逻辑说明:setPageSize(PageSz.A4)把 Word 的页面纸型固定成 A4,setPageMargins传的四个数字是左上右下的页边距。注意单位是 twip,1 英寸等于 1440 twip,所以这里的 1440 就是 1 英寸边距。参数说明:setToWordDefaultTableStyle(true)这句是我强烈建议打开的,它让 HTML 里没写样式或者样式很薄的<table>落到 Word 时,至少套用 Word 默认表格样式,不然出来的表格线是透明的,打印时一脸懵。这三个设置基本够覆盖业务里 90% 的页面要求。
3.3 中文字体与编码的隐藏雷区
XHTML 字符串里的<meta charset="utf-8">别指望它能救你。docx4j 解析 XHTML 时,对没有声明编码的流默认按 UTF-8 处理,这本来是好事,但如果你从老系统接口里拿到的是 GBK 编码的 HTML 字符串,没转码就进来,那转出来的 Word 里中文全会变问号。我的习惯是在入口处统一做一次new String(htmlBytes, StandardCharsets.UTF_8),提前把脏数据洗掉。另外字体名必须写 Word 认识的名字,写font-family: SimSun没问题,写font-family: "宋体"在 LibreOffice 和 WPS 里表现就不稳定,建议直接用英文的字体族名。
4. 图形与表格:图片路径、Base64 内嵌和表格列宽的三个坎
4.1 图片处理:Base64 与远程 URL 的转换差异
HTML 里的图片有两类来源,一是 base64 内嵌到src里,二是远程 URL。docx4j-ImportXHTML 默认支持 base64 内嵌,它能把data:image/png;base64,xxx解析出来,然后作为BinaryPart塞进 Word 的 media 目录。远程 URL 就比较别扭:默认行为下,它不会自动去下载图片,最终的 Word 里只会留下一个空的图片框或者链接文本。
import org.docx4j.convert.xhtml.XHTMLImporterImpl; import org.docx4j.openpackaging.parts.WordprocessingML.BinaryPart; XHTMLImporterImpl importer = new XHTMLImporterImpl(wordMLPackage); importer.setImageHandlerForURL((url) -> { // 这里用 HttpClient 把远程图片拉下来,然后转成 byte[] byte[] imageBytes = fetchRemoteImage(url); BinaryPart binaryPart = new BinaryPart(); binaryPart.setData(imageBytes); return binaryPart; });逻辑说明:这个 Lambda 是XHTMLImporterImpl提供的一个扩展点,叫图片 URL 处理器。当解析器碰到src="http..."这种远程地址时,会回调这个函数,让你自己决定怎么把图片变成二进制数据。参数说明:fetchRemoteImage需要你自己实现,我一般用 Apache HttpClient 或 JDK 自带的HttpURLConnection加超时和 UA 头。注意这里一定要处理网络异常,返回null或者抛错会导致整个转换中断,稳妥做法是在 Lambda 内部 catch 住,返回一个 1x1 的透明占位图字节数组,至少保证 Word 能生成。
4.2 表格样式:把 HTML 表格的边框和宽度搬过去
表格是 XHTML 转 Word 里最容易翻车的区域。HTML 里<table border="1">这种写法,如果不开setToWordDefaultTableStyle(true),转出来是条看不见边框的虚线。开了之后至少有个基础黑线,但宽度布局仍然可能错位。
<table style="width: 100%; border-collapse: collapse;"> <tr> <td style="width: 50%; border: 1px solid #000;">左单元格</td> <td style="width: 50%; border: 1px solid #000;">右单元格</td> </tr> </table>我测试下来,这种行内样式写法比 CSS class 写法在 docx4j 里表现稳定得多。原因不难理解:ImportXHTML 的 CSS 解析器对 class 选择器的支持时好时坏,但行内style属性是直接挂在元素节点上的,解析优先级最高。宽度建议写百分比,不要写像素px,因为 Word 页面实际宽度受页边距影响,写像素经常导致表格溢出到页面外。border-collapse: collapse必须写,不写的话单元格之间会出现双层缝隙。
4.3 分页与页眉页脚:HTML 里没有,Word 里怎么补
XHTML 本身没有“分页符”“页眉”这种概念,docx4j-ImportXHTML 更不会从 HTML 里变出来。但业务需求往往很直接:合同要每页带页码,报告要标题页独立。我的做法是分两步走:第一步,用 XHTML 转换把正文内容全部生成;第二步,用 docx4j 的原生 API 去追加分页符和页脚。
import org.docx4j.wml.Br; import org.docx4j.wml.P; import org.docx4j.wml.R; import org.docx4j.wml.Text; import org.docx4j.wml.Br.Type; P pageBreakPara = new P(); R run = new R(); Br br = new Br(); br.setType(Type.PAGE); run.getContent().add(br); pageBreakPara.getContent().add(run); wordMLPackage.getMainDocumentPart().addObject(pageBreakPara);逻辑说明:Br对象里Type.PAGE就是 Word 的分页符,往段落的run里塞一个,再把这个段落追加到文档末尾,就能强行分页。参数说明:如果要加页眉页脚,别用这种手动方式,正确姿势是用HeaderPart和FooterPart挂到SectPr上,这是另一套 API,但属于必须会的操作,因为纯 XHTML 转换永远产不出页眉页脚。
5. 避坑与常见问题排查:五个真实翻车现场
5.1 现象:生成的 Word 里中文字体全乱了
原因:服务端 Linux 环境没有安装 Word 所用的中文字体,docx4j 在解析时按字体名生成样式,但系统渲染时找不到对应字体文件,Word 打开后就会自动替换成默认字体。解决:别依赖系统字体,直接在 XHTML 的 CSS 里把font-family写成SimSun、SimHei这类具体字体名,确保 docx4j 把字体名写进rFonts标签,让 Word 在客户端自己去找字体。
5.2 现象:图片转出来是一张全黑的方块
原因:XHTMLImporterImpl对 base64 图片的解码逻辑依赖javax.imageio.ImageIO,当图片格式是 WebP 或某些少见编码时,ImageIO 认不出来,就写入了非法字节。解决:在进入转换之前,先用ImageIO.read(ByteArrayInputStream)校验一次图片字节流,读不出来就转成 PNG 再转成 base64 塞回 XHTML。这一步能拦截掉 90% 的黑块问题。
5.3 现象:远程图片怎么都不出来,日志也没有异常
原因:docx4j 对于远程 URL 图片的策略是“默认不处理”,除非你显式调用了setImageHandlerForURL。网上很多老教程没提这个接口,都以为它会自动下载。解决:要么走我 4.1 节写的 Lambda 方案,要么干脆在进入转换前,自己写一个 Java 方法把 XHTML 里的<img src="http...">全部替换成<img src="data:image/png;base64,...">。第二种方案虽然土,但对 docx4j 版本依赖最少。
5.4 现象:表格列宽设置了却不起作用
原因:docx4j 的表格列宽最终由TblGrid里的GridCol和每个单元格的TcW共同决定,XHTML 里的width属性只能影响后者,当前者没正确生成时,Word 就自己均衡列宽。解决:在 XHTML 里给每个<td>写行内宽度样式,同时给<table>写table-layout: fixed;。如果还不行,手动取出Tbl对象,清掉TblGrid重新按比例生成。
5.5 现象:高并发下偶尔生成失败的 docx 文件
原因:WordprocessingMLPackage.createPackage()和对应的XHTMLImporterImpl不是线程安全的,两个线程共用同一个 importer 实例时,底层JaxbElement的上下文会互相覆盖。解决:每个线程里 new 一个新的XHTMLImporterImpl,不要做成 Spring 单例 Bean。如果追求性能,可以复用WordprocessingMLPackage的创建模板,但也必须做 ThreadLocal 隔离。
6. 进阶技巧:封装一个预处理工具类,提前把 HTML 洗成 docx4j 能懂的样子
用久了你会发现,docx4j-ImportXHTML 能不能把活干漂亮,一半取决于你喂给它的 XHTML 干不干净。我现在的习惯是,所有进入转换的 HTML 先过一遍自研的Docx4jXHtmlSanitizer,核心就三个动作:把<link rel="stylesheet">引用的外部 CSS 内容直接内联成<style>;把所有<img>的远程src替换成 base64 数据;把<script>、<iframe>这类无关标签直接剥掉。
import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; public class Docx4jXHtmlPreprocessor { public static String sanitize(String rawHtml) { Document doc = Jsoup.parse(rawHtml); // 移除脚本、iframe、object 等非文档元素 doc.select("script, iframe, object, embed").remove(); // 远程图片全部转成 base64 内嵌 doc.select("img[src^='http']").forEach(img -> { String src = img.attr("src"); byte[] bytes = download(src); String base64 = java.util.Base64.getEncoder().encodeToString(bytes); img.attr("src", "data:image/png;base64," + base64); }); // 把外部样式表内容内联到 style 标签里 doc.select("link[rel='stylesheet']").forEach(link -> { String css = fetchCss(link.attr("href")); doc.head().appendElement("style").attr("type", "text/css").append(css); link.remove(); }); // 统一字体族,避免 WPS 和 Word 显示不一致 doc.select("[style*='font-family']").forEach(el -> { String style = el.attr("style") .replaceAll("font-family:\\s*'?宋体'?;?", "font-family: SimSun;") .replaceAll("font-family:\\s*'?微软雅黑'?;?", "font-family: Microsoft YaHei;"); el.attr("style", style); }); return doc.html(); } }逻辑说明:这个工具类用 Jsoup 做 html 清洗,doc.select(...)负责按 CSS 选择器摘节点。远程图片转 base64 那步,实际是把 4.1 节的下载逻辑提前到字符串层面,好处是 docx4j 那边不再需要配置 image URL handler。参数说明:download和fetchCss两个方法建议加连接超时 3 秒、读超时 5 秒,防止某个外部资源卡死整个转换线程。字体统一那步用正则替换有风险,比如会遇到font-family: Arial, 宋体这种复合写法,我实际用的是按逗号拆分再逐个 map 的方式,这里为了展示逻辑简化了写法。
封装完后,我的调用链就从“直接 importer.convert”变成了importer.convert(sanitize(rawHtml))。从那以后我每次接到“HTML 转 Word”需求,都会先把这个清洗流程强行走一遍,宁可多花几百毫秒,也不让上游的脏 HTML 消耗我的排查时间。docx4j + ImportXHTML 真正稳下来之后,这套方案是能扛住生产环境的,希望帮到你。
本文还有配套的精品资源,点击获取