此方案利用 Word 本身可以保存为 XML 的特性,将设计好的 Word 文档另存为 XML(或 .docx 解压后的 main document 部分),然后在 XML 中嵌入模板标记,最后通过模板引擎生成最终的 Word 文档。
实现原理
-
使用 Microsoft Word 设计好文档样式和固定内容,保存为 Word 2003 XML 文档 (.xml)或直接使用 .docx 包内的
document.xml。 -
在 XML 文件中需要动态插入数据的地方添加模板引擎的占位符(如
${name}、<#list>等)。 -
在 Java 中读取该模板文件,使用 FreeMarker 或 Velocity 渲染,将动态数据填充进去。
-
将渲染后的 XML 内容重新打包成 .docx(如果是操作 .docx 内部 XML)或直接保存为 .doc 格式(如果是 Word 2003 XML)。
示例步骤(以 FreeMarker + docx 为例)
-
设计一个
.docx模板,解压得到word/document.xml。 -
编辑
document.xml,插入 FreeMarker 语法(注意避免破坏 XML 结构,可使用 CDATA 或特殊处理)。 -
在 Java 中,读取
document.xml作为 FreeMarker 模板,传入数据模型生成填充后的 XML 字符串。 -
将该字符串替换回原 .docx 的
document.xml,重新打包为 .docx 文件。java# BeanUtil使用的是Hutool中的工具类 Map<String, Object> dataMap = BeanUtil.beanToMap(report); try { // 1. 创建 FreeMarker 配置 Configuration cfg = new Configuration(Configuration.VERSION_2_3_31); // 模板所在目录 cfg.setDirectoryForTemplateLoading(new File(templateDir)); cfg.setDefaultEncoding("UTF-8"); cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER); cfg.setLogTemplateExceptions(false); cfg.setWrapUncheckedExceptions(true); cfg.setFallbackOnNullLoopVariable(false); // 2. 加载模板(已按上述要求修改的 XML 文件) Template template = cfg.getTemplate(templatePath); // 4. 渲染模板 try (Writer out = new BufferedWriter(new OutputStreamWriter( new FileOutputStream(templateOutputPath), StandardCharsets.UTF_8))) { template.process(dataMap, out); response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document"); String fileName = report.getUnitName() + "年度自评报告.docx"; String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8.toString()).replace("+", "%20"); response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + encodedFileName); # yaml配置 #yearSelfEvaluation: # # 模板目录 # templateDir: G:\\templates # # 模板路径 # templatePath: template\\word\\document.xml # # 输出路径 # templateOutputPath: G:\\templates\\output2.xml # # 模板docx路径 # templateDocxPath: G:\\templates\\template.docx XmlToDocx.convert(templateDocxPath, templateOutputPath, response); } } catch (Exception e) { log.error("渲染模板失败", e); } # XmlToDocx import javax.servlet.http.HttpServletResponse; import java.io.*; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import java.util.zip.ZipOutputStream; public class XmlToDocx { /** * 将渲染后的 Word 2003 XML 文件内容替换到 docx 模板中,并将生成的 docx 写入 HttpServletResponse 输出流 * @param docxTemplate 原始的 docx 模板路径(由 XML 另存得来) * @param renderedXmlPath 渲染后的 XML 文件路径 * @param response HttpServletResponse 对象,用于输出 docx */ public static void convert(String docxTemplate, String renderedXmlPath, HttpServletResponse response) throws IOException { // 读取渲染后的 XML 文件内容(Java 8 兼容) Path xmlPath = Paths.get(renderedXmlPath); byte[] xmlBytes = Files.readAllBytes(xmlPath); String renderedXml = new String(xmlBytes, StandardCharsets.UTF_8); // 创建临时目录存放解压后的文件 Path tempDir = Files.createTempDirectory("docx"); try (ZipInputStream zis = new ZipInputStream(new FileInputStream(docxTemplate))) { ZipEntry entry; while ((entry = zis.getNextEntry()) != null) { File outFile = new File(tempDir.toFile(), entry.getName()); if (entry.isDirectory()) { outFile.mkdirs(); } else { outFile.getParentFile().mkdirs(); try (FileOutputStream fos = new FileOutputStream(outFile)) { byte[] buffer = new byte[8192]; int len; while ((len = zis.read(buffer)) != -1) { fos.write(buffer, 0, len); } } } zis.closeEntry(); } } // 替换 document.xml Path docXmlPath = tempDir.resolve("word/document.xml"); Files.write(docXmlPath, renderedXml.getBytes(StandardCharsets.UTF_8)); // 重新打包为 docx 并直接写入 response 输出流 try (ZipOutputStream zos = new ZipOutputStream(response.getOutputStream())) { Files.walk(tempDir).forEach(path -> { if (Files.isRegularFile(path)) { String entryName = tempDir.relativize(path).toString().replace('\\', '/'); try { zos.putNextEntry(new ZipEntry(entryName)); Files.copy(path, zos); zos.closeEntry(); } catch (IOException e) { throw new UncheckedIOException(e); } } }); zos.finish(); // 确保 ZIP 文件正确结束 } finally { // 清理临时目录 Files.walk(tempDir) .map(Path::toFile) .forEach(File::delete); } } /** * 将渲染后的 Word 2003 XML 文件内容替换到 docx 模板中,生成最终的 docx 文件 * @param docxTemplate 原始的 docx 模板路径(由 XML 另存得来) * @param renderedXmlPath 渲染后的 XML 文件路径 * @param outputDocx 输出 docx 文件路径 */ public static void convert(String docxTemplate, String renderedXmlPath, String outputDocx) throws IOException { // 读取渲染后的 XML 文件内容(Java 8 兼容) Path xmlPath = Paths.get(renderedXmlPath); byte[] xmlBytes = Files.readAllBytes(xmlPath); String renderedXml = new String(xmlBytes, StandardCharsets.UTF_8); // 创建临时目录存放解压后的文件 Path tempDir = Files.createTempDirectory("docx"); try (ZipInputStream zis = new ZipInputStream(new FileInputStream(docxTemplate))) { ZipEntry entry; while ((entry = zis.getNextEntry()) != null) { File outFile = new File(tempDir.toFile(), entry.getName()); if (entry.isDirectory()) { outFile.mkdirs(); } else { outFile.getParentFile().mkdirs(); try (FileOutputStream fos = new FileOutputStream(outFile)) { byte[] buffer = new byte[8192]; int len; while ((len = zis.read(buffer)) != -1) { fos.write(buffer, 0, len); } } } zis.closeEntry(); } } // 替换 document.xml Path docXmlPath = tempDir.resolve("word/document.xml"); Files.write(docXmlPath, renderedXml.getBytes(StandardCharsets.UTF_8)); // 重新打包为 docx try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(outputDocx))) { Files.walk(tempDir).forEach(path -> { if (Files.isRegularFile(path)) { String entryName = tempDir.relativize(path).toString().replace('\\', '/'); try { zos.putNextEntry(new ZipEntry(entryName)); Files.copy(path, zos); zos.closeEntry(); } catch (IOException e) { throw new UncheckedIOException(e); } } }); } finally { // 清理临时目录 Files.walk(tempDir) .map(Path::toFile) .forEach(File::delete); } } public static void main(String[] args) throws IOException { convert("G:\\templates\\template.docx", "G:\\templates\\output2.xml", "F:\\test2.docx"); System.out.println("docx 文件已生成, haha "); } }