Java中使用模板引擎(FreeMarker / Velocity) + Word XML导出复杂Word

此方案利用 Word 本身可以保存为 XML 的特性,将设计好的 Word 文档另存为 XML(或 .docx 解压后的 main document 部分),然后在 XML 中嵌入模板标记,最后通过模板引擎生成最终的 Word 文档。

实现原理

  1. 使用 Microsoft Word 设计好文档样式和固定内容,保存为 Word 2003 XML 文档 (.xml)或直接使用 .docx 包内的 document.xml

  2. 在 XML 文件中需要动态插入数据的地方添加模板引擎的占位符(如 ${name}<#list> 等)。

  3. 在 Java 中读取该模板文件,使用 FreeMarker 或 Velocity 渲染,将动态数据填充进去。

  4. 将渲染后的 XML 内容重新打包成 .docx(如果是操作 .docx 内部 XML)或直接保存为 .doc 格式(如果是 Word 2003 XML)。

示例步骤(以 FreeMarker + docx 为例)

  1. 设计一个 .docx 模板,解压得到 word/document.xml

  2. 编辑 document.xml,插入 FreeMarker 语法(注意避免破坏 XML 结构,可使用 CDATA 或特殊处理)。

  3. 在 Java 中,读取 document.xml 作为 FreeMarker 模板,传入数据模型生成填充后的 XML 字符串。

  4. 将该字符串替换回原 .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 ");
        }
    }
相关推荐
咩咩啃树皮14 小时前
第40篇:Vue3组件化开发精讲——组件拆分、复用、父子通信、工程化架构
java·前端·架构
鱟鲥鳚14 小时前
Spring Boot 集成 LangChain4j:从模型调用到 Tool Calling(Demo版)
java·spring boot
大模型码小白16 小时前
【Python零基础教程】继承、多态与魔法函数:面向对象编程三大核心特性详解
java·大数据·开发语言·人工智能·python·ai编程
腾渊信息科技公司16 小时前
Spring Boot对接MES实战:视觉检测数据自动同步方案
java·人工智能·spring boot·后端·计算机视觉·ai·软件需求
爱笑的源码基地17 小时前
高并发 Redis 缓存门诊HIS系统源码,含财务统计药房进销存
java·程序·门诊系统·诊所系统·云诊所源码
wuqingshun31415918 小时前
TCP超时重传机制是为了解决什么问题?
java
莫逸风20 小时前
【AgentScope 2.0】 0. 学习指南
java·llm·agent·agentscope
z1234567898621 小时前
2026最新两款AI编程工具深度对比实测
java·数据库·ai编程
yaoxin52112321 小时前
470. Java 反射 - Member 接口与 AccessFlag
java·开发语言·python
做个文艺程序员21 小时前
Linux第24篇:Java应用监控体系搭建:Prometheus+Grafana可视化运维
java·grafana·prometheus