资讯动态

docx4j 实现 HTML 转 Word:样式映射、图片嵌入与中文乱码避坑指南

发布时间:2026/9/29 13:56:39 来源:尧图企业网站定制
简介这份资源面向Java后端开发者与办公自动化场景提供使用docx4j配合docx4j-ImportXHTML将HTML转换为Word的完整实现方案解决模板占位符替换、格式保真与多格式导出等常见痛点。压缩包共170个文件约16.71MB以132个xml配置与映射文件、10个java源码、10个class编译文件为主另含8个html模板、3个ttf字体及yaml、md等辅助文件覆盖从模板准备到转换落地的各环节。已有761人学习下载说明该方案在实际项目中具备一定参考价值。读者可从中获取HTML模板设计思路、占位符动态填充逻辑、字体段落列表表格等元素的转换规则以及页眉页脚、页码、水印、目录等增强功能的实现参考并了解如何借助docx4j转换引擎进一步将内容导出为PDF适合需要处理复杂文档转换需求的中高级开发者借鉴。1. 用 docx4j 把 HTML 转 Word为什么它比 POI 更值得押注做过 Java 导出 Word 的人大概率都经历过这样的场景运营给了一段带样式的 HTML 富文本要求原样塞进 Word 文档里表格、图片、加粗、列表一个都不能丢。第一反应是用 Apache POI 的XWPFDocument手写解析结果写了三百行还在处理span stylefont-weight:bold的嵌套最后表格边框还是歪的。这不是 POI 不行而是 POI 的定位是「操作 OOXML 底层结构」它压根没打算帮你做 HTML 到 Word 的语义映射。docx4j 走的是另一条路。它本身是一个基于 JAXB 的 OOXML 对象模型库能把 WordprocessingML 的每个元素映射成 Java 对象而docx4j-ImportXHTML这个扩展模块专门负责把 XHTML 的 DOM 树「翻译」成 docx4j 的 WordprocessingML 对象树。换句话说你给它一段合法的 XHTML它帮你生成段落、表格、图片、超链接你只需要在转换后做少量样式微调。对于「HTML 富文本转 Word」这个需求这套组合是目前 Java 生态里落地成本最低的方案之一。这篇文章面向的是需要把 HTML 内容批量导出成 Word 的后端工程师尤其是做合同生成、报告导出、富文本编辑器内容落地的场景。我会从依赖引入讲到样式映射、图片处理、中文乱码、表格边框这些实际会翻车的地方每一步都给可复现的代码和参数说明。读完你应该能直接在自己的项目里跑通一条 HTML 转 Word 的流水线并且知道哪些坑必须提前绕开。2. docx4j-ImportXHTML 的转换链路与最小可运行工程2.1 依赖坐标与版本选择逻辑docx4j 的依赖管理是这套方案里第一个容易踩坑的地方。它的核心包和 ImportXHTML 模块版本必须对齐否则会出现NoSuchMethodError或者 XHTML 解析器找不到实现类的问题。常见做法是统一用同一个版本号通过 Maven 的dependencyManagement锁定。properties docx4j.version11.4.9/docx4j.version /properties dependencies !-- docx4j 核心提供 WordprocessingML 对象模型 -- dependency groupIdorg.docx4j/groupId artifactIddocx4j-core/artifactId version${docx4j.version}/version /dependency !-- ImportXHTML负责 XHTML DOM 到 WordML 的映射 -- dependency groupIdorg.docx4j/groupId artifactIddocx4j-ImportXHTML/artifactId version${docx4j.version}/version /dependency !-- JAXB 运行时JDK 11 以上必须显式引入 -- dependency groupIdorg.glassfish.jaxb/groupId artifactIdjaxb-runtime/artifactId version2.3.8/version /dependency !-- XHTML 解析器ImportXHTML 内部依赖 -- dependency groupIdnet.sf.saxon/groupId artifactIdSaxon-HE/artifactId version12.4/version /dependency /dependencies这里有几个参数需要说明。docx4j.version建议选 11.x 系列8.x 虽然更老更稳但对 JDK 17 的兼容性差容易出现模块化访问冲突。jaxb-runtime在 JDK 8 里是内置的但 JDK 11 之后被移除不显式引入会在XmlUtils.marshaltoString时报ClassNotFoundException。Saxon 是 XHTML 转换时做 XSLT 处理用的版本 12.x 对应 docx4j 11.x混用 9.x 会导致 XPath 表达式解析异常。提示如果你的项目还在 JDK 8可以省掉 jaxb-runtime但 Saxon 仍然建议显式声明避免容器里自带的旧版本干扰。2.2 从 XHTML 字符串到 Word 文档的最小代码下面这段代码是整个方案的核心骨架输入是一段合法的 XHTML 字符串输出是一个.docx文件。我把它拆成「创建空文档 → 解析 XHTML → 插入内容 → 保存」四步每一步都有对应的对象操作。import org.docx4j.Docx4J; import org.docx4j.convert.in.xhtml.XHTMLImporterImpl; import org.docx4j.openpackaging.packages.WordprocessingMLPackage; import org.docx4j.openpackaging.parts.WordprocessingML.MainDocumentPart; import java.io.File; public class HtmlToWordDemo { public static void convert(String xhtml, String outputPath) throws Exception { // 1. 创建一个空的 Word 文档包内部会初始化 document.xml 等部件 WordprocessingMLPackage wordMLPackage WordprocessingMLPackage.createPackage(); MainDocumentPart mainPart wordMLPackage.getMainDocumentPart(); // 2. 创建 XHTML 导入器绑定到当前文档包 XHTMLImporterImpl importer new XHTMLImporterImpl(wordMLPackage); // 3. 设置图片解析器处理 img src... 的下载与嵌入 importer.setImageHandler(new XHTMLImageHandlerImpl()); // 4. 把 XHTML 转成 WordML 对象列表追加到主文档 mainPart.getContent().addAll(importer.convert(xhtml, null)); // 5. 保存为 docx 文件 wordMLPackage.save(new File(outputPath)); } public static void main(String[] args) throws Exception { String xhtml htmlbody h1季度报告/h1 p本季度营收 strong1200 万/strong同比增长 18%。/p table border1trtd区域/tdtd金额/td/tr trtd华东/tdtd500/td/tr/table /body/html; convert(xhtml, /tmp/report.docx); } }XHTMLImporterImpl的构造函数接收WordprocessingMLPackage是因为导入器需要在转换过程中访问文档包的样式表、关系部件等资源。convert方法的第二个参数是baseUrl当 XHTML 里有相对路径的图片或链接时用它来拼接绝对地址传null表示所有资源都是绝对路径或内联数据。setImageHandler这一步很关键默认的图片处理器只支持data:协议的 base64 内联图片如果你的 HTML 里是http://或file://的图片地址必须自定义 handler否则图片会静默丢失。2.3 XHTML 合法性校验别让脏 HTML 进转换器ImportXHTML 对输入的 XHTML 有格式要求它内部用的是 XML 解析器不是浏览器的容错解析器。这意味着br必须写成br/img srcxxx必须写成img srcxxx/属性值必须加引号标签必须闭合。如果你的 HTML 来自富文本编辑器大概率是不合法的。常见做法是先用 Jsoup 做一轮清洗把 HTML 转成 XHTML 兼容格式import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Entities; public static String cleanToXhtml(String rawHtml) { // 用 Jsoup 解析容错处理脏 HTML Document doc Jsoup.parse(rawHtml); // 设置输出为 XHTML 语法自闭合标签、属性加引号 doc.outputSettings() .syntax(Document.OutputSettings.Syntax.xml) .escapeMode(Entities.EscapeMode.xhtml) .charset(UTF-8); return doc.html(); }Jsoup 的syntax(xml)会把br输出成br /把布尔属性补全成checkedchecked这正是 ImportXHTML 需要的格式。escapeMode(xhtml)保证中文和特殊字符按 XHTML 实体输出避免编码问题。这一步不做后面会遇到SAXParseException: The element type br must be terminated这类报错而且报错信息不会告诉你具体是哪个标签排查起来很痛苦。3. 样式映射、图片嵌入与表格边框的参数调优3.1 CSS 内联样式如何映射到 Word 样式ImportXHTML 对 CSS 的支持是有限度的。它认识font-weight、font-size、color、background-color、text-align、margin、padding这些常用属性但只处理内联样式style...和style块里的简单选择器。外部 CSS 文件、复杂选择器如:nth-child、CSS 变量一律不认。实际项目里我一般会在 HTML 清洗阶段把关键样式内联化。比如富文本编辑器输出的p classtitle我会在 Jsoup 清洗时把.title的样式合并到style属性上// 假设已知 .title 对应 font-size:18pt; font-weight:bold doc.select(p.title).forEach(el - { String existing el.attr(style); el.attr(style, existing ;font-size:18pt;font-weight:bold;); });字号单位要注意ImportXHTML 对px和pt的处理不同。pt会直接映射到 Word 的半点单位half-pointpx会按 96 DPI 换算成 pt换算比例是px * 0.75。如果你希望 Word 里显示 12ptHTML 里写font-size:16px或font-size:12pt都可以但12pt更精确不会有舍入误差。颜色值支持#RRGGBB和rgb(r,g,b)两种格式rgba的 alpha 通道会被忽略。背景色只对段落和表格单元格生效行内元素的背景色不会渲染这是 Word 本身的限制不是 ImportXHTML 的问题。3.2 图片处理的三种来源与对应 Handler图片是 HTML 转 Word 里最容易出问题的部分。ImportXHTML 的XHTMLImageHandler接口只有一个方法getImage(String uri)返回一个AbstractWordXmlPicture对象。你需要根据图片的来源实现不同的获取逻辑。图片来源URI 格式处理方式常见问题base64 内联data:image/png;base64,...默认 handler 支持大图导致内存溢出远程 HTTPhttp://...需自定义下载超时、403、重定向本地文件file:///...需自定义读取路径编码、权限相对路径/images/a.png需 baseUrl 拼接baseUrl 为 null 时报错自定义 handler 的骨架如下import org.docx4j.convert.in.xhtml.XHTMLImageHandler; import org.docx4j.dml.wordprocessingDrawing.Inline; import org.docx4j.openpackaging.packages.WordprocessingMLPackage; import org.docx4j.openpackaging.parts.WordprocessingML.BinaryPartAbstractImage; import org.docx4j.wml.Drawing; public class XHTMLImageHandlerImpl implements XHTMLImageHandler { Override public AbstractWordXmlPicture getImage(String uri) { try { byte[] bytes; if (uri.startsWith(data:)) { // 解析 base64 String base64 uri.substring(uri.indexOf(,) 1); bytes java.util.Base64.getDecoder().decode(base64); } else { // 远程或本地统一用 URL 打开 java.net.URL url new java.net.URL(uri); try (java.io.InputStream in url.openStream()) { bytes in.readAllBytes(); } } // 创建图片部件并返回 BinaryPartAbstractImage imagePart BinaryPartAbstractImage.createImagePart( (WordprocessingMLPackage) null, bytes); return new AbstractWordXmlPictureImpl(imagePart); } catch (Exception e) { // 图片失败不能中断整个转换返回 null 让导入器跳过 return null; } } }这里有个血泪经验getImage返回null时ImportXHTML 会跳过这张图但不会抛异常所以图片下载失败是静默的。如果你发现生成的 Word 里图片少了先检查这个方法的异常日志。另外BinaryPartAbstractImage.createImagePart的第一个参数传null在部分版本里会 NPE稳妥做法是把WordprocessingMLPackage实例通过构造函数传进来。图片尺寸方面ImportXHTML 会读取img的width和height属性单位是 px转换成 EMUEnglish Metric Unit1 px ≈ 9525 EMU。如果 HTML 里没写宽高它会用图片的原始像素尺寸一张 4000px 宽的图会直接撑爆页面。建议在清洗阶段给所有img补上stylemax-width:600px或明确的宽高。3.3 表格边框与列宽的显式设置HTML 表格转 Word 表格边框是最容易「看起来不对」的地方。ImportXHTML 默认会把table border1转成带边框的 Word 表格但边框样式是单线、黑色、0.5pt和网页上的效果有差异。如果你需要更精细的控制比如只保留外边框、设置边框颜色需要在转换后遍历表格对象手动设置。import org.docx4j.wml.*; import javax.xml.bind.JAXBElement; import java.math.BigInteger; public static void setTableBorders(WordprocessingMLPackage pkg) { ListObject tables pkg.getMainDocumentPart() .getJAXBNodesViaXPath(//w:tbl, false); for (Object obj : tables) { Tbl tbl (Tbl) ((JAXBElement?) obj).getValue(); TblPr tblPr tbl.getTblPr(); if (tblPr null) { tblPr new TblPr(); tbl.setTblPr(tblPr); } TblBorders borders new TblBorders(); // 外边框单线4 号大小0.5pt黑色 borders.setTop(border(single, 4, 000000)); borders.setBottom(border(single, 4, 000000)); borders.setLeft(border(single, 4, 000000)); borders.setRight(border(single, 4, 000000)); // 内边框不显示 borders.setInsideH(border(none, 0, auto)); borders.setInsideV(border(none, 0, auto)); tblPr.setTblBorders(borders); } } private static Border border(String val, int sz, String color) { Border b new Border(); b.setVal(val); b.setSz(BigInteger.valueOf(sz)); b.setColor(color); return b; }sz参数的单位是 1/8 pt所以4表示 0.5pt8表示 1pt。val可选single、double、dashed、none。这段代码用 XPath//w:tbl找到所有表格逐个设置边框属性。注意getJAXBNodesViaXPath返回的是JAXBElement包装的对象需要先解包再强转直接强转Tbl会ClassCastException。列宽方面Word 表格的列宽由w:gridCol和每个单元格的w:tcW共同决定。ImportXHTML 会根据 HTML 表格的width属性和各列内容自动分配但经常出现某列过窄导致文字换行。稳妥做法是在 HTML 里给td加width属性或者在转换后手动设置TcW// 设置第一列宽度为 2000 twips约 3.5cm TcPr tcPr cell.getTcPr(); if (tcPr null) { tcPr new TcPr(); cell.setTcPr(tcPr); } TcWidth tcW new TcWidth(); tcW.setW(BigInteger.valueOf(2000)); tcW.setType(dxa); // dxa twips tcPr.setTcW(tcW);dxa是 twips 单位1 twip 1/20 pt2000 twips ≈ 100pt ≈ 3.5cm。这个值需要根据你的页面宽度和列数计算A4 纸默认页边距下正文宽度约 9000 twips。4. 中文乱码、字体缺失与转换性能的避坑清单4.1 中文乱码从编码声明到字体绑定现象生成的 Word 文档里中文显示为方块或乱码英文正常。原因ImportXHTML 在解析 XHTML 时如果输入是String类型它默认按 UTF-8 处理但如果你从文件读取时用了平台默认编码Windows 上是 GBK字符串本身就已经错了。另一个原因是 Word 文档的默认字体是 Calibri不含中文字形需要显式设置东亚字体。解决第一步确保读取 HTML 时显式指定 UTF-8String xhtml new String(Files.readAllBytes(Paths.get(input.html)), StandardCharsets.UTF_8);第二步在转换后设置文档的默认字体把w:eastAsia指向中文字体import org.docx4j.wml.*; // 获取文档默认样式 DocDefaults docDefaults pkg.getMainDocumentPart() .getStyleDefinitionsPart().getJaxbElement() .getDocDefaults(); RFonts rFonts new RFonts(); rFonts.setAscii(Times New Roman); rFonts.setEastAsia(宋体); // 关键东亚字体 rFonts.setHAnsi(Times New Roman); docDefaults.getRPrDefault().getRPr().setRFonts(rFonts);setEastAsia是必须的只设ascii和hAnsi对中文无效。如果文档里混用了多种字体还需要在styles.xml里逐个样式设置或者用XHTMLImporterImpl.setFontFamily在导入时统一指定。4.2 字体缺失服务器上没有中文字体怎么办现象本地开发正常部署到 Linux 服务器后中文变方块。原因Word 文档本身不嵌入字体它只记录字体名称由打开文档的客户端渲染。但如果服务器端需要把 Word 转 PDF比如用 docx4j 的 PDF 导出功能服务器上没装中文字体就会渲染失败。解决在服务器上安装中文字体包或者把字体文件放到项目资源目录通过FontSettings注册// 注册字体目录docx4j 转 PDF 时会从这里查找 org.docx4j.fonts.PhysicalFonts.setRegex(.*(宋体|SimSun|微软雅黑).*); org.docx4j.fonts.PhysicalFonts.addPhysicalFonts( SimSun, new File(/opt/fonts/simsun.ttf));如果只是生成 docx 不转 PDF服务器字体缺失不影响因为字体渲染是客户端的事。但如果你用Docx4J.toPDF()做预览这一步不能省。4.3 大文档转换的内存与超时控制现象转换一个包含 50 张图片的 HTML 时JVM 抛出OutOfMemoryError。原因ImportXHTML 会把所有图片读进内存每张图创建一个BinaryPartAbstractImage如果图片是 base64 内联的原始字符串和解码后的字节数组会同时存在。解决三个层面的控制。第一限制单张图片大小在getImage里判断字节数超过 5MB 就返回 null 并记日志。第二用流式处理不要一次性把所有 HTML 拼成一个巨大字符串而是分章节转换后合并。第三调大 JVM 堆并设置合理的 GCjava -Xmx2g -XX:UseG1GC -XX:MaxGCPauseMillis200 -jar app.jar对于超过 100 页的文档建议拆成多个 docx 再合并docx4j 提供了WordprocessingMLPackage的合并工具类但合并时要注意样式冲突两个文档的同名样式会互相覆盖。4.4 转换后表格跨页断行的处理现象表格在 Word 里跨页时表头不重复或者某一行被从中间截断。原因Word 表格默认允许跨页断行且不重复表头。ImportXHTML 不会自动设置这些属性。解决转换后遍历表格设置表头重复和禁止行内断页// 设置第一行为表头跨页时重复 Tbl tbl ...; TblPr tblPr tbl.getTblPr(); TblPrBase.TblHeader tblHeader new TblPrBase.TblHeader(); tblPr.setTblHeader(tblHeader); // 设置每行禁止跨页断行 for (Tr tr : tbl.getTr()) { TrPr trPr tr.getTrPr(); if (trPr null) { trPr new TrPr(); tr.setTrPr(trPr); } TrPrBase.CantSplit cantSplit new TrPrBase.CantSplit(); trPr.getCnfStyleOrDivIdOrGridBefore().add(cantSplit); }TblHeader让第一行在每页顶部重复CantSplit防止单行被拆到两页。这两个属性在 Word 里对应「重复标题行」和「允许跨页断行」的复选框手动设置比让用户自己调更省事。5. 用 XSLT 做 HTML 预处理与转换结果的自动化校验5.1 用 XSLT 把脏 HTML 规整成 ImportXHTML 友好的 XHTML前面用 Jsoup 做清洗是一种方式但如果你的 HTML 结构复杂、需要做批量规则替换XSLT 更合适。docx4j 内部就是用 XSLT 做 XHTML 到 WordML 的转换你可以在它之前插一道自己的 XSLT把不规范的标签、属性统一掉。比如把b转成strong把i转成em把align属性转成styletext-align:...xsl:stylesheet version2.0 xmlns:xslhttp://www.w3.org/1999/XSL/Transform xsl:output methodxml omit-xml-declarationyes/ !-- 复制所有节点 -- xsl:template match*|node() xsl:copy xsl:apply-templates select*|node()/ /xsl:copy /xsl:template !-- b 转 strong -- xsl:template matchb strongxsl:apply-templates select*|node()//strong /xsl:template !-- align 属性转 style -- xsl:template match*[align] xsl:copy xsl:attribute namestyle xsl:value-of selectconcat(text-align:, align, ;, style)/ /xsl:attribute xsl:apply-templates select*[name()!align]|node()/ /xsl:copy /xsl:template /xsl:stylesheet在 Java 里调用这个 XSLTimport javax.xml.transform.*; import javax.xml.transform.stream.*; TransformerFactory factory TransformerFactory.newInstance(); Transformer transformer factory.newTransformer( new StreamSource(new File(clean.xsl))); transformer.transform( new StreamSource(new StringReader(rawHtml)), new StreamResult(new StringWriter()));XSLT 的好处是规则可以配置化不同来源的 HTML 用不同的 XSLT 文件不用改 Java 代码。缺点是调试麻烦XSLT 报错信息不直观建议先用小样本测试。5.2 转换结果的自动化校验用 XPath 检查关键元素生成 docx 之后怎么确认转换结果符合预期手动打开 Word 看是一种方式但批量场景下不现实。我一般会写一组 XPath 断言在单元测试里跑。import org.docx4j.XmlUtils; import java.util.List; public class DocxAssertions { public static void assertHasHeading(WordprocessingMLPackage pkg, String text) { ListObject results pkg.getMainDocumentPart() .getJAXBNodesViaXPath( //w:p[w:pPr/w:pStyle[w:valHeading1]] [w:r/w:t text ], false); if (results.isEmpty()) { throw new AssertionError(找不到标题: text); } } public static void assertTableCount(WordprocessingMLPackage pkg, int expected) { ListObject tables pkg.getMainDocumentPart() .getJAXBNodesViaXPath(//w:tbl, false); if (tables.size() ! expected) { throw new AssertionError(表格数量不符期望 expected 实际 tables.size()); } } public static void assertImageCount(WordprocessingMLPackage pkg, int expected) { // 图片在 rels 里不在 document.xml 正文 int count pkg.getMainDocumentPart().getRelationshipsPart() .getRelationships().getRelationship().size(); // 这个数字包含样式等关系需要过滤 image 类型 long imageCount pkg.getMainDocumentPart().getRelationshipsPart() .getRelationships().getRelationship().stream() .filter(r - r.getType().contains(image)) .count(); if (imageCount ! expected) { throw new AssertionError(图片数量不符期望 expected 实际 imageCount); } } }这些断言可以放在 CI 里每次修改转换逻辑后自动跑一遍比人工检查靠谱。XPath 里的w:前缀是 WordprocessingML 的命名空间docx4j 的getJAXBNodesViaXPath已经内置了前缀映射直接用就行。5.3 一个我踩过的坑样式名大小写敏感最后说一个很隐蔽的问题。ImportXHTML 在映射h1到 Word 样式时用的是Heading1但如果你在 HTML 里写了h1 stylefont-size:20pt它会创建一个直接格式化direct formatting的段落而不是应用Heading1样式。这导致两个后果一是文档大纲视图里看不到标题层级二是后续想统一改标题样式时改不动。我的习惯是在 XSLT 预处理阶段把h1到h6的style属性剥掉只保留标签本身让 ImportXHTML 走样式映射路径。如果确实需要微调字号在转换后修改styles.xml里的Heading1样式定义而不是在 HTML 里写内联样式。这个习惯帮我省了很多「为什么标题样式改不了」的排查时间。转换完成后用pkg.getMainDocumentPart().getStyleDefinitionsPart()拿到样式部件检查Heading1是否存在如果不存在说明 ImportXHTML 没有触发样式映射需要回头检查 HTML 里是不是有干扰的内联样式。希望这些经验能帮你在 HTML 转 Word 这条路上少走几个弯路。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑