【Java实战】SpringBoot 集成 freemarker 导出 Word 模板
-
- 一、前言
-
- [1.1 应用场景](#1.1 应用场景)
- [1.2 本文目标](#1.2 本文目标)
- [1.3 环境说明](#1.3 环境说明)
- 二、核心原理简述
- 三、实战步骤
-
- [3.1 第一步:引入 Maven 依赖](#3.1 第一步:引入 Maven 依赖)
- [3.2 第二步:制作 Word 模板(doc 格式)并转换为 ftl 格式](#3.2 第二步:制作 Word 模板(doc 格式)并转换为 ftl 格式)
-
- [3.2.1 制作 doc 模板](#3.2.1 制作 doc 模板)
- [3.2.2 doc 转 ftl 格式](#3.2.2 doc 转 ftl 格式)
- [3.3 第三步:编写核心代码](#3.3 第三步:编写核心代码)
-
- [3.3.1 Freemarker 工具类](#3.3.1 Freemarker 工具类)
- [3.3.2 接口层(Controller)](#3.3.2 接口层(Controller))
- [3.4 第四步:放置 ftl 模板文件](#3.4 第四步:放置 ftl 模板文件)
- 四、测试验证
-
- [4.1 接口调用测试](#4.1 接口调用测试)
- [4.2 导出文件验证](#4.2 导出文件验证)
- 五、常见问题
- 六、总结
本文聚焦 SpringBoot 集成 spring-boot-starter-freemarker 实现 Word 模板导出实战,全程实际开发场景,包含 doc 模板转 ftl 格式、模板数据填充、代码实现、测试验证,附详细步骤+代码+截图,新手也能直接上手复用,适合 Java 后端开发学习和项目落地。
一、前言
1.1 应用场景
在后端开发中,Word 导出是高频需求(如报表导出、合同导出、单据导出、数据统计报告等),而 freemarker 作为一款模板引擎,能快速实现 Word 模板的动态数据填充,搭配 SpringBoot 可高效落地到项目中,相比其他方式更简洁、易维护。
1.2 本文目标
- 掌握 SpringBoot 集成 spring-boot-starter-freemarker 的核心配置
- 学会将 doc 格式 Word 模板转换为 freemarker 支持的 ftl 格式
- 实现 Word 模板数据动态填充、文件导出功能
- 完成测试验证,解决导出过程中的常见问题
1.3 环境说明
本文实战环境,可直接对应你的本地环境,无需额外修改:
- JDK:8 及以上
- SpringBoot:2.7.x(适配多数项目,其他版本可微调配置)
- freemarker:spring-boot-starter-freemarker 内置版本(无需额外指定版本)
- 工具:IDEA、WPS/Office(编辑 Word 模板)、浏览器(测试接口)
- 测试文件:doc 格式模板文件、转换后的 ftl 模板文件
二、核心原理简述
freemarker 导出 Word 的核心是:
1.先制作 Word 模板(doc 格式),标记需要动态填充的占位符(如:${name});
2.再将其转换为 freemarker 支持的 ftl 模板文件;
3.SpringBoot 集成 freemarker 后,需要读取 ftl 模板,我通过代码封装需要填充的数据(Map/实体类),再结合 freemarker 引擎渲染模板,最终转换为 Word 文件并响应给前端进行下载。
关键要点:ftl 模板的占位符语法、doc 转 ftl 的格式兼容、数据填充的语法规范、文件流的正确处理。
三、实战步骤
3.1 第一步:引入 Maven 依赖
在 SpringBoot 项目的 pom.xml 中,引入 spring-boot-starter-freemarker 依赖,无需额外引入 freemarker 核心包(starter 已集成),同时引入文件处理相关依赖。
java
<!-- 核心:freemarker 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
以下是其他所需的依赖:
java
<!-- commons-io 依赖 -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>
<!-- Hutool 工具类 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.16</version>
</dependency>
<!-- easyExcel 导入导出工具类 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.3.2</version>
</dependency>
<dependency>
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-base</artifactId>
<version>4.1.3</version>
</dependency>
<dependency>
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-web</artifactId>
<version>4.1.3</version>
</dependency>
<dependency>
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-annotation</artifactId>
<version>4.1.3</version>
</dependency>
3.2 第二步:制作 Word 模板(doc 格式)并转换为 ftl 格式
这是核心步骤之一,模板的制作直接影响导出效果,重点是正确设置占位符,避免格式错乱。
3.2.1 制作 doc 模板
- 用 WPS/Office 新建 Word 文档(保存为 doc 格式,注意:不要保存为 docx 格式,避免转换后格式异常);
- 在需要动态填充数据的位置,设置占位符,占位符语法: 变量名,例如:姓名: {变量名},例如:姓名: 变量名,例如:姓名:{name}、年龄:${age};
- 模板中可保留固定内容(如标题、表格表头、落款等),仅将动态数据替换为占位符;
具体如下图所示:

3.2.2 doc 转 ftl 格式
将制作好的 doc 模板文件转换为 freemarker 支持的 ftl 格式,步骤如下:
- 将 doc 模板文件另存为「Word 2003 XML 文档」(后缀为 .xml);
- 找到保存后的 .xml 文件,将文件后缀名改为 .ftl;
- 用 IDEA 打开 ftl 文件,检查占位符是否正常(若有乱码,调整文件编码为 UTF-8)。
注:转换后不要随意修改 ftl 中的标签结构,仅修改占位符相关内容,否则会导致导出的 Word 格式错乱。

3.3 第三步:编写核心代码
核心代码分为 3 部分:
1.实体类 / Map(封装填充数据,目前我测试使用,直接使用Map类型封装)
2.工具类(freemarker 模板渲染、文件流处理)
3.接口层(提供导出接口,供前端调用)
以下是核心代码:
3.3.1 Freemarker 工具类
java
import cn.afterturn.easypoi.word.WordExportUtil;
import cn.hutool.core.lang.Assert;
import freemarker.template.Configuration;
import freemarker.template.Template;
import org.apache.commons.io.IOUtils;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.Base64;
import java.util.Map;
import java.util.Objects;
public class ExportWordUtils {
private static final Logger LOGGER = LoggerFactory.getLogger(ExportWordUtils.class);
/**
* 导出word(基于FreeMarker)
*
* @param dataMap 数据集
* @param templateName 模板名称
* @param filePath 模板路径
* @param fileName 文件名
* @param request HttpServletRequest
* @param response HttpServletResponse
*/
public static void exportDoc(Map<String, Object> dataMap, String templateName,
String filePath, String fileName,
HttpServletRequest request, HttpServletResponse response) {
Assert.notNull(dataMap, "数据集不能为空");
Assert.notNull(templateName, "模板名称不能为空");
Assert.notNull(filePath, "模板路径不能为空");
Assert.notNull(fileName, "文件名不能为空");
Writer writer = null;
try {
Configuration config = new Configuration(Configuration.VERSION_2_3_30);
config.setDefaultEncoding(StandardCharsets.UTF_8.name());
config.setDirectoryForTemplateLoading(new File(filePath));
Template template = config.getTemplate(templateName, StandardCharsets.UTF_8.name());
String userAgent = getUserAgent(request);
String encodedFileName = encodeFileName(fileName + ".doc", userAgent);
response.setContentType("application/xml");
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.addHeader("Content-Disposition", "attachment;fileName=" + fileName);
writer = new BufferedWriter(new OutputStreamWriter(response.getOutputStream(), StandardCharsets.UTF_8));
template.process(dataMap, writer);
writer.flush();
} catch (Exception e) {
LOGGER.error("导出Word文档失败,模板: {}", templateName, e);
if (!response.isCommitted()) {
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
} finally {
IOUtils.closeQuietly(writer);
}
}
/**
* 获取模板文件路径
* 兼容 Windows 和 Linux 系统
*
* @return 模板所在目录的绝对路径
*/
public static String getTemplatePath() {
try {
String path = Objects.requireNonNull(
Thread.currentThread().getContextClassLoader().getResource("templates/word/"))
.getPath();
return java.net.URLDecoder.decode(path, "UTF-8");
} catch (Exception e) {
LOGGER.error("获取模板路径失败", e);
throw new RuntimeException("获取模板路径失败", e);
}
}
private static String getUserAgent(HttpServletRequest request) {
return request.getHeader("User-Agent");
}
private static String encodeFileName(String fileName, String userAgent) throws UnsupportedEncodingException {
if (userAgent.contains("MSIE") || userAgent.contains("Trident")) {
return URLEncoder.encode(fileName, "UTF-8");
} else if (userAgent.contains("Firefox")) {
return new String(fileName.getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1);
} else {
return URLEncoder.encode(fileName, "UTF-8");
}
}
}
3.3.2 接口层(Controller)
编写接口,封装需要填充的数据,调用工具类实现导出功能,前端可通过浏览器或接口工具(Postman)调用:
java
@Slf4j
@RestController
public class TestController {
private static final Logger LOGGER = LoggerFactory.getLogger(TestController.class);
@GetMapping("/exportWord")
public void exportWord(HttpServletResponse response, HttpServletRequest request) {
String fileName = "测试单";
Map<String, Object> dataMap = new HashMap<>();
dataMap.put("year", String.valueOf(DateUtils.getCurrentYear()));
dataMap.put("month", String.valueOf(DateUtils.getCurrentMonth()));
dataMap.put("day", String.valueOf(DateUtils.getCurrentDay()));
dataMap.put("name", "张三");
dataMap.put("age", "20");
dataMap.put("phone", "123456789");
dataMap.put("address", "北京市东城区长安街北侧");
dataMap.put("hobby", "学习");
String templatePath = ExportWordUtils.getTemplatePath();
LOGGER.info("导出测试单,模板路径: {}, 文件名: {}", templatePath, fileName);
// 调用工具类导出 Word
ExportWordUtils.exportDoc(dataMap, "test.ftl", templatePath, fileName, request, response);
}
}
3.4 第四步:放置 ftl 模板文件
将转换好的 ftl 模板文件,放到项目 /resources/templates/word/ 路径下,确保模板路径正确,否则会报「模板找不到」异常。

四、测试验证
验证1:接口调用测试
验证2:导出文件验证
4.1 接口调用测试
- 启动 SpringBoot 项目,确保项目无报错;
- 用浏览器或 Postman 调用导出接口(如:http://localhost:9995/exportWord);
- 观察是否自动下载 Word 文件,无报错即接口调用成功。

4.2 导出文件验证
- 打开下载的 Word 文件,检查占位符是否被正确替换为填充的数据;
- 检查 Word 格式是否正常(无乱码、表格对齐、字体样式一致);
- 验证数据是否正常回填显示。

五、常见问题
结合实际开发中遇到的问题,整理如下:
- 问题1:模板找不到(Template not found)
解决方案:检查 ftl 模板路径配置是否正确,模板文件名是否正确,路径是否有拼写错误。 - 问题2:导出的 Word 文件乱码
解决方案:确保 ftl 模板编码为 UTF-8,接口响应头设置正确的 charset=UTF-8,工具类中文件流编码统一为 UTF-8。 - 问题3:doc 转 ftl 后格式错乱
解决方案:制作 doc 模板时尽量简化格式,避免复杂排版;转换后不要修改 ftl 中的 XML 标签结构,仅调整占位符。 - 问题4:导出文件无法打开
解决方案:确保模板是 doc 格式转换的(不要用 docx),ftl 模板无语法错误,占位符语法是否格式正确(有时候doc转成xml格式后,表达式会被样式代码拆分,需要手动删除多余样式,调整为正确的表达式${变量名}),数据填充时避免出现null,会导致模板渲染失败。
六、总结
本文完成了 SpringBoot 集成 spring-boot-starter-freemarker 导出 Word 模板的完整实战,从依赖引入、模板制作(doc 转 ftl)、代码实现,到测试验证、避坑指南,所有代码可直接复制复用。
核心要点:
- doc 模板转 ftl 是关键,需要注意格式兼容和占位符语法正确。
- 获取模板文件路径要正确,文件编码要正确,避免模板找不到、乱码问题。
- 数据填充时,变量名需与 ftl 模板占位符完全一致。
- ftl中模板占位符要对应有数据,如果存在null值或没有配置数据,会导致报错。