一、前言
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 已集成),同时引入文件处理相关依赖。
<!-- 核心:freemarker 依赖 -->
<dependency>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-starter-freemarker</artifactid>
</dependency>以下是其他所需的依赖:
<!-- 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 工具类
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)调用:
@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值或没有配置数据,会导致报错。
以上就是springboot集成freemarker导出word模板的实战步骤的详细内容,更多关于springboot freemarker导出word模板的资料请关注代码网其它相关文章!
发表评论