SpringBoot3實現(xiàn)Word文檔動態(tài)生成與下載
引言
日常開發(fā)中,經(jīng)常會遇到這樣的需求:根據(jù)業(yè)務(wù)數(shù)據(jù)動態(tài)生成Word文檔,比如合同導(dǎo)出、報表生成、用戶證明材料等。如果直接用原生API操作Word,代碼繁瑣且容易出現(xiàn)格式錯亂,后期維護成本極高。
最近在SpringBoot3項目中,通過集成 poi-tl 工具,摸索出一套極簡高效的Word動態(tài)生成方案,無需復(fù)雜的樣式編碼配置,僅需簡單幾步,即可快速實現(xiàn)Word模板占位符填充、動態(tài)表格渲染及圖片插入等核心需求,大幅提升開發(fā)效率。
項目代碼結(jié)構(gòu)
先附上完整的項目代碼結(jié)構(gòu),方便大家對照搭建,后續(xù)所有代碼都將對應(yīng)此結(jié)構(gòu),避免路徑錯亂、類找不到等問題:
src
└── main
├── java
│ └── com.example.demo
│ ├── controller
│ │ └── ContractController.java <-- 接口類(接收請求、調(diào)用工具類)
│ ├── dto
│ │ ├── ContractDTO.java <-- 主數(shù)據(jù)模型(對應(yīng)模板占位符)
│ │ └── ContractDetailDTO.java <-- 明細數(shù)據(jù)模型(對應(yīng)表格占位符)
│ ├── util
│ │ └── WordGenerateUtil.java <-- 通用工具類(封裝生成、下載邏輯)
│ └── DemoApplication.java <-- 項目啟動類
└── resources
├── static
│ └── img
│ └── attachment.jpg <-- 測試圖片(用于圖片渲染驗證)
├── templates
│ └── contractTemplate.docx <-- Word模板(存放占位符)
└── application.yml <-- 項目配置文件(默認配置即可)一、環(huán)境準(zhǔn)備
- JDK 17+;
- Spring Boot 3.0+(本文用 3.2.5);
- poi-tl 1.12.2;
1.1 引入Maven依賴
直接在pom.xml中添加以下依賴:
<!-- 核心依賴:實現(xiàn)Word模板渲染與生成 -->
<dependency>
<groupId>com.deepoove</groupId>
<artifactId>poi-tl</artifactId>
<version>1.12.2</version>
</dependency>
<!-- Apache POI: 處理Office文檔的核心庫 -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.5.1</version>
</dependency>
<!-- 可選但推薦:文件操作工具 -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.21.0</version>
</dependency>1.2 模板與圖片準(zhǔn)備
1. 模板準(zhǔn)備:在src/main/resources目錄下,新建templates文件夾,放入Word模板文件(后綴必須是.docx,不能是.doc,否則會報格式錯誤),命名為contractTemplate.docx;
2. 圖片準(zhǔn)備:在src/main/resources/static目錄下,新建img文件夾,放入一張測試圖片(命名為attachment.jpg),用于后續(xù)圖片渲染驗證。
二、實現(xiàn)Word動態(tài)生成與下載
整體流程:制作Word模板(設(shè)置占位符)→ 編寫數(shù)據(jù)模型(與占位符對應(yīng))→ 編寫工具類與接口(實現(xiàn)生成與下載)。
2.1 第一步:制作Word模板
模板制作的核心是設(shè)置“占位符”,后續(xù)代碼將數(shù)據(jù)替換到占位符中,且會完全繼承模板的原有樣式(字體、顏色、行距等),無需額外編寫樣式代碼。
制作規(guī)則(簡單好記,無需死記硬背):
- 普通文本占位符:用 {{變量名}} 表示,比如 {{contractNo}}(合同編號)、{{customerName}}(客戶姓名);
- 動態(tài)表格占位符:用 {{#表格變量名}} 開頭;
- 圖片占位符:用 {{@圖片變量名}} 表示,后續(xù)通過代碼傳入圖片流即可正常渲染;
- 占位符可以放在Word的任何位置(正文、表格、頁眉頁腳)。
實戰(zhàn)示例(以客戶合同模板為例):
打開WPS/Word,新建文檔,輸入以下內(nèi)容并插入占位符,保存為contractTemplate.docx,放入templates目錄:
客戶合同
合同編號:{{contractNo}}
客戶姓名:{{customerName}}
聯(lián)系電話:{{phone}}
簽訂日期:{{signDate}}
合同明細:
{{#detailList}}
合同附件:{{@attachmentImg}}2.2 第二步:編寫數(shù)據(jù)模型
數(shù)據(jù)模型的作用是封裝需要填充到模板中的數(shù)據(jù),變量名必須和模板中的占位符完全一致(大小寫敏感)。
實戰(zhàn)代碼(兩個核心類,放在dto包下):
import lombok.Data;
import java.util.List;
import java.math.BigDecimal;
/**
* 合同主數(shù)據(jù)模型(對應(yīng)模板中的普通文本占位符)
*/
@Data
public class ContractDTO {
// 合同編號(對應(yīng){{contractNo}})
private String contractNo;
// 客戶姓名(對應(yīng){{customerName}})
private String customerName;
// 聯(lián)系電話(對應(yīng){{phone}})
private String phone;
// 簽訂日期(對應(yīng){{signDate}})
private String signDate;
// 合同明細(對應(yīng){{#detailList}}循環(huán)表格)
private List<ContractDetailDTO> detailList;
// 附件圖片(對應(yīng){{@attachmentImg}},無需賦值,接口中單獨處理)
private String attachmentImg;
}
/**
* 合同明細數(shù)據(jù)模型(對應(yīng)表格中的占位符)
*/
@Data
public class ContractDetailDTO {
// 商品名稱(對應(yīng){{productName}})
private String productName;
// 單價(對應(yīng){{price}})
private BigDecimal price;
// 數(shù)量(對應(yīng){{num}})
private Integer num;
// 小計(對應(yīng){{total}})
private BigDecimal total;
}2.3 編寫通用Word工具類
工具類封裝了“生成Word并下載”“生成Word保存到本地”兩個核心方法。
import com.deepoove.poi.XWPFTemplate;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.util.Map;
/**
* Word文檔生成工具類(通用,可直接復(fù)用)
*/
@Component
public class WordGenerateUtil {
/**
* 生成Word并通過瀏覽器下載
* @param templateName 模板文件名(放在resources/templates目錄下)
* @param data 填充到模板的數(shù)據(jù)(Map格式,key對應(yīng)模板占位符)
* @param response 響應(yīng)對象(用于返回下載流)
* @param downloadFileName 下載時的文件名(如:張三的合同.docx)
* @throws IOException 異常(可在調(diào)用處統(tǒng)一處理)
*/
public void generateAndDownload(String templateName, Map<String, Object> data,
HttpServletResponse response, String downloadFileName) throws IOException {
// 1. 讀取templates目錄下的Word模板
ClassPathResource resource = new ClassPathResource("templates/" + templateName);
// 2. 編譯模板并填充數(shù)據(jù)
XWPFTemplate template = XWPFTemplate.compile(resource.getInputStream()).render(data);
// 3. 設(shè)置響應(yīng)頭,實現(xiàn)瀏覽器下載(解決中文文件名亂碼問題)
response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(downloadFileName, "UTF-8"));
response.setCharacterEncoding("UTF-8");
// 4. 寫入響應(yīng)流,完成下載
try (OutputStream outputStream = response.getOutputStream()) {
template.write(outputStream);
outputStream.flush();
} finally {
// 5. 關(guān)閉資源,避免內(nèi)存泄漏
template.close();
}
}
/**
* 生成Word并保存到本地(可選,根據(jù)需求使用)
* @param templateName 模板文件名
* @param data 填充數(shù)據(jù)
* @param localPath 本地保存路徑(如:D:/contract/張三的合同.docx)
* @throws IOException 異常
*/
public void generateToLocal(String templateName, Map<String, Object> data, String localPath) throws IOException {
ClassPathResource resource = new ClassPathResource("templates/" + templateName);
XWPFTemplate template = XWPFTemplate.compile(resource.getInputStream()).render(data);
// 寫入本地文件
template.writeToFile(localPath);
template.close();
}
}2.4 編寫接口類
編寫REST接口,模擬從數(shù)據(jù)庫獲取數(shù)據(jù)(實際開發(fā)中替換為真實DAO查詢),調(diào)用工具類實現(xiàn)Word下載。
import cn.iocoder.boot.entity.ContractDTO;
import cn.iocoder.boot.entity.ContractDetailDTO;
import cn.iocoder.boot.utils.WordGenerateUtil;
import com.deepoove.poi.data.*;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.core.io.ClassPathResource;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.*;
/**
* 合同導(dǎo)出接口(實戰(zhàn)示例)
*/
@RestController
@RequestMapping("/contract")
public class ContractController {
@Resource
private WordGenerateUtil wordGenerateUtil;
/**
* 導(dǎo)出單個客戶合同
* @param customerId 客戶ID(實際開發(fā)中用于查詢客戶數(shù)據(jù))
* @param response 響應(yīng)對象(返回下載流)
* @throws IOException 異常
*/
@GetMapping("/export/{customerId}")
public void exportContract(@PathVariable String customerId, HttpServletResponse response) throws IOException {
// 1. 模擬從數(shù)據(jù)庫查詢客戶合同數(shù)據(jù)(實際開發(fā)中替換為真實DAO查詢)
ContractDTO contractDTO = getContractData(customerId);
// 2. 組裝數(shù)據(jù)(key必須和模板占位符完全一致)
Map<String, Object> data = new HashMap<>();
data.put("contractNo", contractDTO.getContractNo());
data.put("customerName", contractDTO.getCustomerName());
data.put("phone", contractDTO.getPhone());
data.put("signDate", contractDTO.getSignDate());
// 3. 創(chuàng)建表格數(shù)據(jù)
RowRenderData row0 = Rows.of("商品名稱", "單價(元)","數(shù)量","小計(元)").textColor("FFFFFF")
.bgColor("4472C4").center().create();
Tables.TableBuilder tableBuilder = Tables.of(row0);
contractDTO.getDetailList().forEach(detail -> {
RowRenderData row = Rows.create(detail.getProductName(), detail.getPrice().toString(), detail.getNum().toString(), detail.getTotal().toString());
tableBuilder.addRow(row);
});
data.put("detailList", tableBuilder.create());
// 4. 填充圖片,圖片路徑:項目resources/static/img目錄下的圖片(實際可從數(shù)據(jù)庫獲取圖片路徑)
data.put("attachmentImg", Pictures.ofStream(
new ClassPathResource("static/img/attachment.jpg").getInputStream(), // 圖片流
PictureType.JPEG) // 圖片格式,無需手動寫后綴
.size(200, 100) // 圖片寬高(單位:像素)
.create()
);
// 5. 調(diào)用工具類,生成并下載Word
wordGenerateUtil.generateAndDownload(
"contractTemplate.docx", // 模板文件名
data, // 填充數(shù)據(jù)
response, // 響應(yīng)對象
contractDTO.getCustomerName() + "的合同.docx" // 下載文件名
);
}
/**
* 模擬查詢合同數(shù)據(jù)(實際開發(fā)中替換為真實業(yè)務(wù)邏輯/DAO查詢)
*/
private ContractDTO getContractData(String customerId) {
ContractDTO contract = new ContractDTO();
// 模擬主數(shù)據(jù)(實際從數(shù)據(jù)庫查詢)
contract.setContractNo("HT-" + System.currentTimeMillis());
contract.setCustomerName("張三");
contract.setPhone("13800138000");
contract.setSignDate("2026-03-25");
// 模擬合同明細數(shù)據(jù)(對應(yīng)表格循環(huán))
List<ContractDetailDTO> detailList = new ArrayList<>();
ContractDetailDTO detail1 = new ContractDetailDTO();
detail1.setProductName("Java開發(fā)服務(wù)");
detail1.setPrice(new BigDecimal("5000.00"));
detail1.setNum(1);
detail1.setTotal(new BigDecimal("5000.00"));
ContractDetailDTO detail2 = new ContractDetailDTO();
detail2.setProductName("系統(tǒng)維護服務(wù)");
detail2.setPrice(new BigDecimal("2000.00"));
detail2.setNum(1);
detail2.setTotal(new BigDecimal("2000.00"));
detailList.add(detail1);
detailList.add(detail2);
contract.setDetailList(detailList);
return contract;
}
}三、測試驗證
測試步驟簡單,無需復(fù)雜配置,啟動SpringBoot項目后,直接訪問接口即可驗證功能是否正常。
3.1 前置準(zhǔn)備
1. 確認templates目錄下有contractTemplate.docx模板,static/img目錄下有attachment.jpg圖片;
2. 確保項目啟動無報錯(JDK17環(huán)境,依賴正常引入);
3. 無需修改application.yml,默認配置即可。
3.2 接口訪問與驗證
訪問接口地址:http://localhost:8080/contract/export/1(customerId隨便傳,此處僅為模擬),瀏覽器會自動下載Word文件。
四、常見問題
- 模板后綴必須是.docx,不能是.doc,否則會報“不支持的格式”異常;
- 占位符大小寫敏感,比如模板中是{{contractNo}},代碼中寫contractno會導(dǎo)致填充失??;
- 圖片渲染需通過Pictures工具類創(chuàng)建渲染對象,僅傳字符串路徑會導(dǎo)致圖片無法顯示;項目內(nèi)圖片用ClassPathResource獲取流,本地圖片用FileInputStream獲取流;
- 批量生成Word時,必須循環(huán)關(guān)閉template資源,否則會導(dǎo)致內(nèi)存溢出;
五、擴展場景
實際開發(fā)中,除了基礎(chǔ)的文本、表格、圖片填充,還可能遇到以下場景,簡單補充實現(xiàn)思路:
- 條件渲染:某些字段為空時不顯示,可使用{{?變量名}} 占位符(如{{?remark}} 備注:{{remark}} {{/?remark}});
- 動態(tài)圖片:從數(shù)據(jù)庫獲取圖片流(無需保存到本地),直接傳入Pictures.ofStream()方法即可渲染;
- 批量導(dǎo)出:循環(huán)調(diào)用工具類的generateToLocal方法,生成多個Word文件,再通過ZipOutputStream打包成zip,返回給前端下載。
六、總結(jié)
本次實戰(zhàn)用SpringBoot3實現(xiàn)Word動態(tài)生成與下載,核心邏輯是“模板占位符+數(shù)據(jù)模型+通用工具類”,無需復(fù)雜的樣式配置,所有代碼均可直接復(fù)制復(fù)用。
對比原生API,這種方式不僅代碼簡潔,而且后期維護方便——修改模板無需改代碼,只需調(diào)整Word文件中的占位符和樣式即可,極大降低維護成本。
以上就是SpringBoot3實現(xiàn)Word文檔動態(tài)生成與下載的詳細內(nèi)容,更多關(guān)于SpringBoot3 Word動態(tài)生成與下載的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
SpringBoot中使用Filter和Interceptor的示例代碼
這篇文章主要介紹了SpringBoot中使用Filter和Interceptor的示例代碼,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-06-06
解決使用json-lib包實現(xiàn)xml轉(zhuǎn)json時空值被轉(zhuǎn)為空中括號的問題
網(wǎng)上能查到的xml轉(zhuǎn)json的jar包大部分是net.sf.json-lib,但是JSON json =xmlSerializer.read(xml); 方法會出現(xiàn)將空值轉(zhuǎn)化為[]的問題,下面為大家提供兩種解決方法2018-03-03
SpringBoot 使用 OpenAPI3 規(guī)范整合 knife4j的詳細過程
Swagger工具集使用OpenAPI規(guī)范,可以生成、展示和測試基于OpenAPI規(guī)范的API文檔,并提供了生成客戶端代碼的功能,本文給大家介紹SpringBoot使用OpenAPI3規(guī)范整合knife4j的詳細過程,感興趣的朋友跟隨小編一起看看吧2023-12-12
使用Cloud Toolkit在IDEA中極速創(chuàng)建dubbo工程
這篇文章主要介紹了使用Cloud Toolkit在IDEA中極速創(chuàng)建dubbo工程,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-11-11

