Word工具类——实现导出自定义Word文档(基于FreeMarker模板引擎生成动态内容的Word文档)

一、引言

在 Java Web 开发中,经常需要将动态数据(如报表、合同、表单等)导出为自定义格式的 Word 文档。本文将介绍一套基于 FreeMarker 模板引擎实现的 Word 导出工具类,支持通过模板 + 数据模型的方式快速生成个性化 Word 文档,无需复杂的 POI 操作,上手简单、扩展性强。

核心优势:

  • 基于 FreeMarker 模板引擎,动态填充数据,支持复杂文档结构(表格、列表、动态段落等)
  • 工具类封装完整,提供一键导出接口,无需重复编码
  • 自动处理临时文件、中文编码、浏览器响应头等问题,避免常见坑
  • 支持自定义模板,灵活适配不同业务场景(报表、合同、简历等)

二、环境准备:添加FreeMarker依赖

首先,在项目的pom.xml中添加FreeMarker依赖:

复制代码
<!-- FreeMarker 模板引擎依赖 -->
<dependency>
    <groupId>org.freemarker</groupId>
    <artifactId>freemarker</artifactId>
    <version>2.3.30</version>
</dependency>

三、核心工具类实现

下面是我们完整的Word文档导出工具类,包含了详细的注释和最佳实践:

复制代码
import freemarker.template.Configuration;
import freemarker.template.Template;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;

/**
 * Word文档导出工具类
 * 基于FreeMarker模板引擎生成动态内容的Word文档
 */
public class ExportWordUtil {

    /**
     * FreeMarker配置对象(静态常量)
     * 整个应用共享同一个配置实例,提高性能
     */
    private static final Configuration configuration;
    /**
     * 模板文件存放的基础目录路径
     * 相对于classpath的路径(即resources目录下),用于定位FreeMarker模板文件
     */
    private static final String TEMPLATE_DIR = "/template/word/";

    /**
     * 静态初始化块 - 在类加载时执行一次
     * 初始化FreeMarker配置,设置编码、模板加载路径等
     */
    static {
        // 创建FreeMarker配置实例
        configuration = new Configuration();
        // 设置编码:使用UTF-8编码处理中文字符,避免乱码
        configuration.setEncoding(Locale.CHINA, StandardCharsets.UTF_8.name());
        // 设置模板加载路径:从类路径下的指定目录加载模板文件
        configuration.setClassForTemplateLoading(ExportWordUtil.class, TEMPLATE_DIR);
        // 禁用模板异常日志:避免在控制台输出不必要的模板解析错误
        configuration.setLogTemplateExceptions(false);
        // 包装未检查异常:将运行时异常包装成TemplateException,便于统一处理
        configuration.setWrapUncheckedExceptions(true);
    }


    /**
     * 导出自定义Word文档
     *
     * @param response       HttpServletResponse对象,用于向客户端输出文件
     * @param dataMap        数据模型Map,包含要填充到模板中的动态数据
     * @param exportFileName 导出文件的名称(不含扩展名)
     * @param ftlName        FreeMarker模板文件名(如:report.ftl)
     */
    public static void exportWord(HttpServletResponse response, Map<String, Object> dataMap, String exportFileName, String ftlName) {
        // 临时文件引用,用于后续清理
        File file = null;
        try {
            // 步骤1:加载FreeMarker模板
            // 根据模板名称从配置的目录中获取模板对象
            Template template = configuration.getTemplate(ftlName);
            // 步骤2:生成Word文档临时文件
            // 将数据模型填充到模板中,生成实际的Word文档文件
            file = createDocFile(dataMap, template);
            // 步骤3:设置HTTP响应头,告诉浏览器这是一个要下载的文件
            // 对文件名进行URL编码,确保包含中文等特殊字符时能正确传输
            String fileName = URLEncoder.encode(exportFileName + ".docx", "UTF-8");
            // 设置响应编码为UTF-8
            response.setCharacterEncoding("UTF-8");
            // 设置内容类型为Microsoft Word文档
            response.setContentType("application/msword");
            // 设置Content-Disposition头,强制浏览器下载文件而不是直接打开
            // "attachment"表示附件,filename指定下载时显示的文件名
            response.setHeader("Content-Disposition", "attachment;filename=\"" + fileName + "\"");
            // 步骤4:将生成的Word文件写入HTTP响应输出流
            // 使用try-with-resources语法自动关闭资源,避免内存泄漏
            try (InputStream inputStream = new FileInputStream(file);
                 ServletOutputStream out = response.getOutputStream()) {
                // 创建缓冲区,提高大文件传输效率
                byte[] buffer = new byte[8192];
                // 每次实际读取的字节数
                int bytesRead;
                // 循环读取文件内容并写入响应输出流
                // read()返回-1表示已到达文件末尾
                while ((bytesRead = inputStream.read(buffer)) != -1) {
                    out.write(buffer, 0, bytesRead);
                }
                // 刷新输出流,确保所有数据都发送到客户端
                out.flush();
            }
        } catch (Exception e) {
            throw new RuntimeException("导出自定义Word文档失败,原因:{}", e);
        } finally {
            // 步骤5:清理临时文件
            // 无论导出成功还是失败,都要删除临时文件,避免磁盘空间浪费
            if (file != null && file.exists()) {
                file.delete();
            }
        }
    }


    /**
     * 创建Word文档临时文件
     * 将数据模型填充到模板,生成实际的.docx文件
     *
     * @param dataMap  数据模型,包含模板中需要的所有动态数据
     * @param template FreeMarker模板对象
     * @return 生成的临时Word文件
     */
    private static File createDocFile(Map<String, Object> dataMap, Template template) throws Exception {
        // 生成随机文件名,避免多用户同时下载时文件名冲突
        String randomFileName = "document_" + UUID.randomUUID();
        // 在系统的临时目录中创建临时文件
        // createTempFile参数:文件名前缀、后缀、保存目录(null表示默认临时目录)
        File tempDocxFile = File.createTempFile(randomFileName, ".docx");
        //使用try-with-resources语句,这样可以自动关闭资源,避免手动关闭可能出现的错误,并且代码更简洁。
        try (OutputStreamWriter outputStreamWriter = new OutputStreamWriter(
                new FileOutputStream(tempDocxFile), StandardCharsets.UTF_8)) {
            // 核心步骤:将数据模型与模板结合,生成最终内容
            // process()方法将dataMap中的数据填充到template模板中
            // 结果通过outputStreamWriter写入临时文件
            template.process(dataMap, outputStreamWriter);
            // 刷新写入器,确保所有数据都写入文件
            outputStreamWriter.flush();
        }
        // 返回生成的临时文件
        return tempDocxFile;
    }


}

四、使用示例

4.1 制作Word模板

4.1.1创建模板内容

  • 使用Microsoft Word创建一个文档

  • 在需要动态填充数据的位置使用${variableName}格式的占位符

  • 例如:${name}${age}${class}


模板制作的内容如下:

4.1.2保存模板

  • 完成模板设计后,选择"文件" → "另存为"

  • 选择文件类型为"XML文档 (*.xml)"

  • 注意:必须是XML格式,不能是普通的.docx格式

4.2 保存模板到项目

4.2.1将模板移动到resources目录

  • 将保存好的XML文件复制到src/main/resources/template/word/目录下

  • 确保目录结构与工具类中TEMPLATE_DIR常量的值一致

4.2.2格式化模板文件

  • 在IDEA中打开XML文件

  • Ctrl+Alt+L(Windows/Linux)或Cmd+Option+L(Mac)格式化代码

  • 这一步确保XML结构清晰,方便阅读

4.3 编写测试代码

复制代码
@RequestMapping("testWord")
public void testWord(HttpServletResponse response) throws Exception {

    Map<String, Object> data = new LinkedHashMap<>();
    data.put("name", "张三");
    data.put("age", 12);
    data.put("class", "五年级");
    data.put("grade", "男");
    data.put("province", "北京市");
    
    // 调用工具类导出Word文档
    // 参数说明:response响应对象、data数据模型、"导出测试"文件名、"test.xml"模板名
    ExportWordUtil.exportWord(response, data, "导出测试", "test.xml");
}

4.4 下载模板,查看效果

  • 访问对应的接口地址

  • 浏览器会自动下载生成的Word文档

  • 打开文档检查数据是否正确填充


文件打开如下:

五、常见问题与解决方案

5.1 导出文件损坏或无法打开

问题现象:下载的Word文档无法正常打开,提示文件损坏。

解决方案

  1. 将模板文件的后缀从.xml改为.ftl

  2. 在IDEA中右键点击文件,选择"Refactor" → "Rename"

  3. 修改后缀名为.ftl

  4. 在代码中调用时使用新的文件名,如:ExportWordUtil.exportWord(response, data, "导出测试", "test.ftl")

原因分析:某些情况下,FreeMarker对.xml后缀的文件处理方式不同,改为.ftl后缀可以避免这个问题。

5.2 中文乱码问题

解决方案

  1. 确保工具类中的编码设置为UTF-8

  2. 检查模板文件是否保存为UTF-8编码

  3. 在响应头中正确设置字符编码

5.3 模板找不到或加载失败

解决方案

  1. 检查模板文件是否放在正确的目录:src/main/resources/template/word/

  2. 确认TEMPLATE_DIR常量的值与实际目录结构匹配

  3. 确保模板文件名与代码中引用的名称完全一致(包括大小写)

相关推荐
Hx_Ma1620 小时前
Map集合的5种遍历方式
java·前端·javascript
小手cool20 小时前
Java 列表中查找最小值和最大值最有效率的方法
java
惊讶的猫20 小时前
多线程同步问题及解决
java·开发语言·jvm
wfsm20 小时前
工厂模式创建动态代理实现类
java·开发语言
好好研究21 小时前
总结SSM设置欢迎页的方式
xml·java·后端·mvc
Hui Baby21 小时前
java -jar 启动原理
java·pycharm·jar
weixin_5112552121 小时前
更新jar内资源和代码
java·jar
木井巳21 小时前
【递归算法】验证二叉搜索树
java·算法·leetcode·深度优先·剪枝
不当菜虚困21 小时前
windows下HSDB导出class文件报错【java.io.IOException : 系统找不到指定的路径。】
java·开发语言
小马爱打代码21 小时前
Spring Boot:第三方 API 调用的企业级容错设计
java·spring boot·后端