SpringBoot - 实现将HTML内容转换为Word文档教程(使用docx4j生成docx文件)
作者:hangge | 2025-11-20 08:46
项目中我们有时需要将将 HTML(或 XHTML)转换为 Word,例如:导出富文本编辑器内容、生成报告、将网页内容归档为可编辑的 DOCX 等。docx4j 是 Java 生态中操作 Office Open XML(.docx)的成熟库,提供了从 XHTML/HTML 到 WordML 的转换工具,下面我将通过样例演示如何使用 docx4j 实现服务器端将 HTML 内容转换生成 DOCX。
(2)接着我们要创建一个空白的 docx 文件(这里我命名为 template.docx),并将其放置在项目的 resources 目录下。

(2)关键代码说明:
(2)我们使用 Postman 测试这个 /export-docx 接口,传入 html 内容和文件名,并将结果保存到本地。


1,准备工作
(1)首先我们需要在项目的 pom.xml 中添加 docx4j 以及相关的依赖。
<!-- docx4j核心依赖 --> <dependency> <groupId>org.docx4j</groupId> <artifactId>docx4j-core</artifactId> <version>11.4.8</version> </dependency> <!-- HTML 转 DOCX,需要这个 --> <dependency> <groupId>org.docx4j</groupId> <artifactId>docx4j-ImportXHTML</artifactId> <version>11.4.8</version> </dependency> <dependency> <groupId>org.docx4j</groupId> <artifactId>docx4j-JAXB-ReferenceImpl</artifactId> <version>11.4.8</version> <!-- 或最新 Jakarta 版本 --> </dependency> <!-- 用于解析/修改 HTML(处理 img) --> <dependency> <groupId>org.jsoup</groupId> <artifactId>jsoup</artifactId> <version>1.16.1</version> </dependency> <!-- apache commons-io:简化文件/流操作 --> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.14.0</version> </dependency> <!-- 添加 JAXB 依赖(JDK 11+) --> <dependency> <groupId>org.glassfish.jaxb</groupId> <artifactId>jaxb-runtime</artifactId> <version>4.0.3</version> </dependency> <dependency> <groupId>jakarta.xml.bind</groupId> <artifactId>jakarta.xml.bind-api</artifactId> <version>4.0.0</version> </dependency>
提示:这么做是因为 HTML → DOCX 的渲染需要预定义样式。docx4j 官方推荐使用一个“正常的 Word 文档模板”来导入 HTML,由于其自带样式、表格、字体等,导出效果最接近 Word。

2,样例代码
(1)我这里创建一个 Controller,该 Controller 暴露 /export-docx 接口,接收形如 { html: "...", filename?: "xxx.docx" } 的 JSON 数据,返回 application/vnd.openxmlformats-officedocument.wordprocessingml.document 二进制流。
import org.docx4j.openpackaging.packages.WordprocessingMLPackage;
import org.docx4j.convert.in.xhtml.XHTMLImporterImpl;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.apache.commons.io.FileUtils;
import java.io.*;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.UUID;
@RestController
@CrossOrigin
public class ExportController {
/**
* 接收 HTML,转换为 docx 并返回
* 请求体例子:
* { "html": "<html>...</html>", "filename": "report.docx" }
*/
@PostMapping("/export-docx")
public ResponseEntity<byte[]> exportDocx(@RequestBody ExportRequest req) {
String html = req.getHtml();
String filename = (req.getFilename() == null || req.getFilename().isEmpty()) ?
("document-" + System.currentTimeMillis() + ".docx") : req.getFilename();
// 将 html 进行预处理:处理图片 data: URI 和远程图片(下载为临时文件,替换 src)
File[] tmpFiles = new File[0];
try {
Document doc = Jsoup.parse(html);
// 处理 <img> 标签
Elements imgs = doc.select("img");
if (!imgs.isEmpty()) {
tmpFiles = new File[imgs.size()];
int idx = 0;
for (Element img : imgs) {
String src = img.attr("src");
if (src == null || src.isEmpty()) continue;
// data:image/...;base64,...
if (src.startsWith("data:")) {
// parse base64 data URI
String base64Part = src.substring(src.indexOf(",") + 1);
byte[] data = Base64.getDecoder().decode(base64Part);
String ext = guessExtensionFromData(src); // png/jpg...
File tmp = File.createTempFile("img_" + UUID.randomUUID(), "." + ext);
FileUtils.writeByteArrayToFile(tmp, data);
tmpFiles[idx++] = tmp;
img.attr("src", tmp.toURI().toString()); // file://...
} else if (src.startsWith("http://") || src.startsWith("https://")) {
// download remote image to temp file
URL url = new URL(src);
String ext = guessExtensionFromUrl(src); // try to infer ext
File tmp = File.createTempFile("img_" + UUID.randomUUID(),
ext != null ? "." + ext : ".img");
try (InputStream in = url.openStream()) {
FileUtils.copyInputStreamToFile(in, tmp);
tmpFiles[idx++] = tmp;
img.attr("src", tmp.toURI().toString());
} catch (Exception ex) {
// 下载失败则忽略图片(或保留原 src),不阻塞全局转换
ex.printStackTrace();
}
} else {
// 其他相对路径或 file:// 等,保留或根据需要处理
}
}
}
// Optional: 取回修改过的完整 html(包含内联 <head><style> 的建议)
// 如果用户传入的是片段(没有 <html>),Jsoup.parse 会补齐
String processedHtml = doc.outerHtml();
// 先处理自闭合标签
processedHtml = processedHtml.replaceAll("<(img|meta|br|hr)([^>]*)>", "<$1$2/>");
processedHtml = processedHtml.replaceAll("<(col)(?!group)([^>]*)>", "<$1$2/>");
// 加载带默认样式的 template.docx
InputStream is = getClass().getResourceAsStream("/template.docx");
WordprocessingMLPackage wordMLPackage = WordprocessingMLPackage.load(is);
// 使用 docx4j 将 HTML 导入 docx
XHTMLImporterImpl xHTMLImporter = new XHTMLImporterImpl(wordMLPackage);
// 若需要可以传入 base uri:xHTMLImporter.setHyperlinkStyle ??
// 导入 HTML 内容
wordMLPackage.getMainDocumentPart().getContent().addAll(
xHTMLImporter.convert(processedHtml, null)
);
// 保存到 byte[]
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
wordMLPackage.save(baos);
byte[] bytes = baos.toByteArray();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.parseMediaType(
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"));
headers.setContentDispositionFormData("attachment", filename);
return ResponseEntity.ok()
.headers(headers)
.body(bytes);
}
} catch (Exception ex) {
ex.printStackTrace();
return ResponseEntity.status(500).body(("Conversion failed: "
+ ex.getMessage()).getBytes(StandardCharsets.UTF_8));
} finally {
// 清理临时文件
if (tmpFiles != null) {
for (File f : tmpFiles) {
if (f != null && f.exists()) f.delete();
}
}
}
}
// 从 data: URI 头部猜测图片格式
private static String guessExtensionFromData(String dataUri) {
// data:image/png;... => png
try {
String mime = dataUri.substring(5, dataUri.indexOf(";"));
if (mime.contains("/")) {
return mime.substring(mime.indexOf("/") + 1);
}
} catch (Exception ignored) {}
return "png";
}
// 从 URL 路径猜测图片格式
private static String guessExtensionFromUrl(String url) {
try {
String lower = url.toLowerCase();
if (lower.contains(".png")) return "png";
if (lower.contains(".jpg") || lower.contains(".jpeg")) return "jpg";
if (lower.contains(".gif")) return "gif";
if (lower.contains(".svg")) return "svg";
if (lower.contains(".bmp")) return "bmp";
} catch (Exception ignored) {}
return null;
}
// DTO
public static class ExportRequest {
private String html;
private String filename;
public String getHtml() { return html; }
public void setHtml(String html) { this.html = html; }
public String getFilename() { return filename; }
public void setFilename(String filename) { this.filename = filename; }
}
}
(2)关键代码说明:
- 我这里使用了 Jsoup 把 HTML 解析并把 data:/http(s): 图片下载成临时文件,然后替换 <img src=""> 为 file:// 路径。XHTMLImporterImpl 在导入时可以读取 file:// 路径并把图片内嵌到 docx。
- wordMLPackage.save(baos) 会把生成好的 docx 写入 ByteArrayOutputStream,然后直接返回给前端下载。
- 临时文件在 finally 中尝试删除,注意生产环境要考虑安全与并发清理策略(可以放到专门 tmp 目录并定期清理)。
- 如果 HTML 包含外部 CSS 文件,建议前端先把关键样式内联或把 <style> 放到传入 HTML 的 <head> 中,这会提升导出的样式保真度。
3,运行测试
(1)假设我需要将如下 html 页面内容转换成 word:
<html>
<head>
<meta charset="utf-8" />
<style>
body {
font-family: "Arial", sans-serif;
font-size: 12p;
}
table {
border-collapse: collapse;
}
th {
background: #F9FAFB;
font-weight: 600;
}
td,
th {
border: 1px solid #ccc;
padding: 4px;
}
</style>
</head>
<body>
<p>欢迎访问<a href="https://www.hangge.com">hangge.com</a></p>
<p><img src="https://www.hangge.com/blog/images/logo.png" alt="logo"></p>
<p></p>
<p>测试<strong>加粗</strong>测试<em>斜体</em>测试<u>下划线</u>测试<s>删除线</s></p>
<p></p>
<p><span style="color: rgb(255, 102, 255);">修改文字颜色</span></p>
<p><span style="font-size: 22px;">调整文字大小</span></p>
<p></p>
<p>下面是一个3x3表格</p>
<table>
<thead>
<tr>
<th>ID</th>
<th>用户名称</th>
<th>用户年龄</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>刘小龙</td>
<td>88</td>
</tr>
<tr>
<td>2</td>
<td>张大伟</td>
<td>66</td>
</tr>
</tbody>
</table>
</body>
</html>
(2)我们使用 Postman 测试这个 /export-docx 接口,传入 html 内容和文件名,并将结果保存到本地。

(3)打开保存下来的 report.docx 文件,可以看到该 word 文档内容如下:

(4)特别要说明的是,我们提交的 html 数据也可以只包含 body 里面的内容,后端会自动补齐 <html> 等内容。

全部评论(0)