SpringBoot集成freemarker導(dǎo)出Word模板的實戰(zhàn)步驟
一、前言
1.1 應(yīng)用場景
在后端開發(fā)中,Word 導(dǎo)出是高頻需求(如報表導(dǎo)出、合同導(dǎo)出、單據(jù)導(dǎo)出、數(shù)據(jù)統(tǒng)計報告等),而 freemarker 作為一款模板引擎,能快速實現(xiàn) Word 模板的動態(tài)數(shù)據(jù)填充,搭配 SpringBoot 可高效落地到項目中,相比其他方式更簡潔、易維護。
1.2 本文目標
- 掌握 SpringBoot 集成 spring-boot-starter-freemarker 的核心配置
- 學會將 doc 格式 Word 模板轉(zhuǎn)換為 freemarker 支持的 ftl 格式
- 實現(xiàn) Word 模板數(shù)據(jù)動態(tài)填充、文件導(dǎo)出功能
- 完成測試驗證,解決導(dǎo)出過程中的常見問題
1.3 環(huán)境說明
本文實戰(zhàn)環(huán)境,可直接對應(yīng)你的本地環(huán)境,無需額外修改:
- JDK:8 及以上
- SpringBoot:2.7.x(適配多數(shù)項目,其他版本可微調(diào)配置)
- freemarker:spring-boot-starter-freemarker 內(nèi)置版本(無需額外指定版本)
- 工具:IDEA、WPS/Office(編輯 Word 模板)、瀏覽器(測試接口)
- 測試文件:doc 格式模板文件、轉(zhuǎn)換后的 ftl 模板文件
二、核心原理簡述
freemarker 導(dǎo)出 Word 的核心是:
1.先制作 Word 模板(doc 格式),標記需要動態(tài)填充的占位符(如:${name});
2.再將其轉(zhuǎn)換為 freemarker 支持的 ftl 模板文件;
3.SpringBoot 集成 freemarker 后,需要讀取 ftl 模板,我通過代碼封裝需要填充的數(shù)據(jù)(Map/實體類),再結(jié)合 freemarker 引擎渲染模板,最終轉(zhuǎn)換為 Word 文件并響應(yīng)給前端進行下載。
關(guān)鍵要點:ftl 模板的占位符語法、doc 轉(zhuǎn) ftl 的格式兼容、數(shù)據(jù)填充的語法規(guī)范、文件流的正確處理。
三、實戰(zhàn)步驟
3.1 第一步:引入 Maven 依賴
在 SpringBoot 項目的 pom.xml 中,引入 spring-boot-starter-freemarker 依賴,無需額外引入 freemarker 核心包(starter 已集成),同時引入文件處理相關(guān)依賴。
<!-- 核心: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 導(dǎo)入導(dǎo)出工具類 -->
<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 格式)并轉(zhuǎn)換為 ftl 格式
這是核心步驟之一,模板的制作直接影響導(dǎo)出效果,重點是正確設(shè)置占位符,避免格式錯亂。
3.2.1 制作 doc 模板
- 用 WPS/Office 新建 Word 文檔(保存為 doc 格式,注意:不要保存為 docx 格式,避免轉(zhuǎn)換后格式異常);
- 在需要動態(tài)填充數(shù)據(jù)的位置,設(shè)置占位符,占位符語法: 變量名,例如:姓名: {變量名},例如:姓名: 變量名,例如:姓名:{name}、年齡:${age};
- 模板中可保留固定內(nèi)容(如標題、表格表頭、落款等),僅將動態(tài)數(shù)據(jù)替換為占位符;
具體如下圖所示:

3.2.2 doc 轉(zhuǎn) ftl 格式
將制作好的 doc 模板文件轉(zhuǎn)換為 freemarker 支持的 ftl 格式,步驟如下:
- 將 doc 模板文件另存為「Word 2003 XML 文檔」(后綴為 .xml);
- 找到保存后的 .xml 文件,將文件后綴名改為 .ftl;
- 用 IDEA 打開 ftl 文件,檢查占位符是否正常(若有亂碼,調(diào)整文件編碼為 UTF-8)。
注:轉(zhuǎn)換后不要隨意修改 ftl 中的標簽結(jié)構(gòu),僅修改占位符相關(guān)內(nèi)容,否則會導(dǎo)致導(dǎo)出的 Word 格式錯亂。

3.3 第三步:編寫核心代碼
核心代碼分為 3 部分:
1.實體類 / Map(封裝填充數(shù)據(jù),目前我測試使用,直接使用Map類型封裝)
2.工具類(freemarker 模板渲染、文件流處理)
3.接口層(提供導(dǎo)出接口,供前端調(diào)用)
以下是核心代碼:
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);
/**
* 導(dǎo)出word(基于FreeMarker)
*
* @param dataMap 數(shù)據(jù)集
* @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, "數(shù)據(jù)集不能為空");
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("導(dǎo)出Word文檔失敗,模板: {}", templateName, e);
if (!response.isCommitted()) {
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
} finally {
IOUtils.closeQuietly(writer);
}
}
/**
* 獲取模板文件路徑
* 兼容 Windows 和 Linux 系統(tǒng)
*
* @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)
編寫接口,封裝需要填充的數(shù)據(jù),調(diào)用工具類實現(xiàn)導(dǎo)出功能,前端可通過瀏覽器或接口工具(Postman)調(diào)用:
@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", "北京市東城區(qū)長安街北側(cè)");
dataMap.put("hobby", "學習");
String templatePath = ExportWordUtils.getTemplatePath();
LOGGER.info("導(dǎo)出測試單,模板路徑: {}, 文件名: {}", templatePath, fileName);
// 調(diào)用工具類導(dǎo)出 Word
ExportWordUtils.exportDoc(dataMap, "test.ftl", templatePath, fileName, request, response);
}
}
3.4 第四步:放置 ftl 模板文件
將轉(zhuǎn)換好的 ftl 模板文件,放到項目 /resources/templates/word/ 路徑下,確保模板路徑正確,否則會報「模板找不到」異常。

四、測試驗證
驗證1:接口調(diào)用測試
驗證2:導(dǎo)出文件驗證
4.1 接口調(diào)用測試
- 啟動 SpringBoot 項目,確保項目無報錯;
- 用瀏覽器或 Postman 調(diào)用導(dǎo)出接口(如:http://localhost:9995/exportWord);
- 觀察是否自動下載 Word 文件,無報錯即接口調(diào)用成功。

4.2 導(dǎo)出文件驗證
- 打開下載的 Word 文件,檢查占位符是否被正確替換為填充的數(shù)據(jù);
- 檢查 Word 格式是否正常(無亂碼、表格對齊、字體樣式一致);
- 驗證數(shù)據(jù)是否正?;靥铒@示。

五、常見問題
結(jié)合實際開發(fā)中遇到的問題,整理如下:
- 問題1:模板找不到(Template not found)
解決方案:檢查 ftl 模板路徑配置是否正確,模板文件名是否正確,路徑是否有拼寫錯誤。 - 問題2:導(dǎo)出的 Word 文件亂碼
解決方案:確保 ftl 模板編碼為 UTF-8,接口響應(yīng)頭設(shè)置正確的 charset=UTF-8,工具類中文件流編碼統(tǒng)一為 UTF-8。 - 問題3:doc 轉(zhuǎn) ftl 后格式錯亂
解決方案:制作 doc 模板時盡量簡化格式,避免復(fù)雜排版;轉(zhuǎn)換后不要修改 ftl 中的 XML 標簽結(jié)構(gòu),僅調(diào)整占位符。 - 問題4:導(dǎo)出文件無法打開
解決方案:確保模板是 doc 格式轉(zhuǎn)換的(不要用 docx),ftl 模板無語法錯誤,占位符語法是否格式正確(有時候doc轉(zhuǎn)成xml格式后,表達式會被樣式代碼拆分,需要手動刪除多余樣式,調(diào)整為正確的表達式${變量名}),數(shù)據(jù)填充時避免出現(xiàn)null,會導(dǎo)致模板渲染失敗。
六、總結(jié)
本文完成了 SpringBoot 集成 spring-boot-starter-freemarker 導(dǎo)出 Word 模板的完整實戰(zhàn),從依賴引入、模板制作(doc 轉(zhuǎn) ftl)、代碼實現(xiàn),到測試驗證、避坑指南,所有代碼可直接復(fù)制復(fù)用。
核心要點:
- doc 模板轉(zhuǎn) ftl 是關(guān)鍵,需要注意格式兼容和占位符語法正確。
- 獲取模板文件路徑要正確,文件編碼要正確,避免模板找不到、亂碼問題。
- 數(shù)據(jù)填充時,變量名需與 ftl 模板占位符完全一致。
- ftl中模板占位符要對應(yīng)有數(shù)據(jù),如果存在null值或沒有配置數(shù)據(jù),會導(dǎo)致報錯。
以上就是SpringBoot集成freemarker導(dǎo)出Word模板的實戰(zhàn)步驟的詳細內(nèi)容,更多關(guān)于SpringBoot freemarker導(dǎo)出Word模板的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
RestTemplate使用Proxy代理作為跳板發(fā)送請求
這篇文章主要為大家介紹了RestTemplate使用代理proxy作為跳板發(fā)送請求的方法示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步2022-03-03
Javaweb EL自定義函數(shù)開發(fā)及代碼實例
這篇文章主要介紹了Javaweb EL自定義函數(shù)開發(fā)及代碼實例,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2020-06-06
Java將List轉(zhuǎn)換為String的幾種方式
我們大家在實際開發(fā)中經(jīng)常遇到List轉(zhuǎn)為String字符串的情況,下面這篇文章主要給大家介紹了關(guān)于Java將List轉(zhuǎn)換為String的幾種方式,文中通過實例代碼介紹的非常詳細,需要的朋友可以參考下2023-05-05
SpringMVC中解決@ResponseBody注解返回中文亂碼問題
這篇文章主要介紹了SpringMVC中解決@ResponseBody注解返回中文亂碼問題, 小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-04-04
springboot中@Async默認線程池導(dǎo)致OOM問題
這篇文章主要介紹了springboot中@Async默認線程池導(dǎo)致OOM問題,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-06-06
聊聊ResourceBundle和properties讀取配置文件的區(qū)別
這篇文章主要介紹了ResourceBundle和properties讀取配置文件的區(qū)別,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-07-07

