一、引言
在 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文档无法正常打开,提示文件损坏。
解决方案:
-
将模板文件的后缀从
.xml改为.ftl -
在IDEA中右键点击文件,选择"Refactor" → "Rename"
-
修改后缀名为
.ftl -
在代码中调用时使用新的文件名,如:
ExportWordUtil.exportWord(response, data, "导出测试", "test.ftl")
原因分析:某些情况下,FreeMarker对.xml后缀的文件处理方式不同,改为.ftl后缀可以避免这个问题。

5.2 中文乱码问题
解决方案:
-
确保工具类中的编码设置为UTF-8
-
检查模板文件是否保存为UTF-8编码
-
在响应头中正确设置字符编码
5.3 模板找不到或加载失败
解决方案:
-
检查模板文件是否放在正确的目录:
src/main/resources/template/word/ -
确认
TEMPLATE_DIR常量的值与实际目录结构匹配 -
确保模板文件名与代码中引用的名称完全一致(包括大小写)