日常开发中,经常会遇到这样的需求:根据业务数据动态生成Word文档,比如合同导出、报表生成、用户证明材料等。如果直接用原生API操作Word,代码繁琐且容易出现格式错乱,后期维护成本极高。
最近在SpringBoot3项目中,通过集成 poi-tl工具,摸索出一套极简高效的Word动态生成方案,无需复杂的样式编码配置,仅需简单几步,即可快速实现Word模板占位符填充、动态表格渲染及图片插入等核心需求,大幅提升开发效率。
项目代码结构
先附上完整的项目代码结构,方便大家对照搭建,后续所有代码都将对应此结构,避免路径错乱、类找不到等问题:
src
└── main
├── java
│ └── com.example.demo
│ ├── controller
│ │ └── ContractController.java <-- 接口类(接收请求、调用工具类)
│ ├── dto
│ │ ├── ContractDTO.java <-- 主数据模型(对应模板占位符)
│ │ └── ContractDetailDTO.java <-- 明细数据模型(对应表格占位符)
│ ├── util
│ │ └── WordGenerateUtil.java <-- 通用工具类(封装生成、下载逻辑)
│ └── DemoApplication.java <-- 项目启动类
└── resources
├── static
│ └── img
│ └── attachment.jpg <-- 测试图片(用于图片渲染验证)
├── templates
│ └── contractTemplate.docx <-- Word模板(存放占位符)
└── application.yml <-- 项目配置文件(默认配置即可)
一、环境准备
-
JDK 17+;
-
Spring Boot 3.0+(本文用 3.2.5);
-
poi-tl 1.12.2;
1.1 引入Maven依赖
直接在pom.xml中添加以下依赖:
<!-- 核心依赖:实现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 模板与图片准备
-
模板准备:在src/main/resources目录下,新建templates文件夹,放入Word模板文件(后缀必须是.docx,不能是.doc,否则会报格式错误),命名为contractTemplate.docx;
-
图片准备:在src/main/resources/static目录下,新建img文件夹,放入一张测试图片(命名为attachment.jpg),用于后续图片渲染验证。
二、实现Word动态生成与下载
整体流程:制作Word模板(设置占位符)→ 编写数据模型(与占位符对应)→ 编写工具类与接口(实现生成与下载)。
2.1 第一步:制作Word模板
模板制作的核心是设置"占位符",后续代码将数据替换到占位符中,且会完全继承模板的原有样式(字体、颜色、行距等),无需额外编写样式代码。
制作规则(简单好记,无需死记硬背):
-
普通文本占位符:用 {{变量名}} 表示,比如 {{contractNo}}(合同编号)、{{customerName}}(客户姓名);
-
动态表格占位符:用 {{#表格变量名}} 开头;
-
图片占位符:用 {{@图片变量名}} 表示,后续通过代码传入图片流即可正常渲染;
-
占位符可以放在Word的任何位置(正文、表格、页眉页脚)。
实战示例(以客户合同模板为例):
打开WPS/Word,新建文档,输入以下内容并插入占位符,保存为contractTemplate.docx,放入templates目录:
客户合同
合同编号:{{contractNo}}
客户姓名:{{customerName}}
联系电话:{{phone}}
签订日期:{{signDate}}
合同明细:
{{#detailList}}
合同附件:{{@attachmentImg}}
2.2 第二步:编写数据模型
数据模型的作用是封装需要填充到模板中的数据,变量名必须和模板中的占位符完全一致(大小写敏感)。
实战代码(两个核心类,放在dto包下):
import lombok.Data;
import java.util.List;
import java.math.BigDecimal;
/**
* 合同主数据模型(对应模板中的普通文本占位符)
*/
@Data
public class ContractDTO {
// 合同编号(对应{{contractNo}})
private String contractNo;
// 客户姓名(对应{{customerName}})
private String customerName;
// 联系电话(对应{{phone}})
private String phone;
// 签订日期(对应{{signDate}})
private String signDate;
// 合同明细(对应{{#detailList}}循环表格)
private List<ContractDetailDTO> detailList;
// 附件图片(对应{{@attachmentImg}},无需赋值,接口中单独处理)
private String attachmentImg;
}
/**
* 合同明细数据模型(对应表格中的占位符)
*/
@Data
public class ContractDetailDTO {
// 商品名称(对应{{productName}})
private String productName;
// 单价(对应{{price}})
private BigDecimal price;
// 数量(对应{{num}})
private Integer num;
// 小计(对应{{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文档生成工具类(通用,可直接复用)
*/
@Component
public class WordGenerateUtil {
/**
* 生成Word并通过浏览器下载
* @param templateName 模板文件名(放在resources/templates目录下)
* @param data 填充到模板的数据(Map格式,key对应模板占位符)
* @param response 响应对象(用于返回下载流)
* @param downloadFileName 下载时的文件名(如:张三的合同.docx)
* @throws IOException 异常(可在调用处统一处理)
*/
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. 编译模板并填充数据
XWPFTemplate template = XWPFTemplate.compile(resource.getInputStream()).render(data);
// 3. 设置响应头,实现浏览器下载(解决中文文件名乱码问题)
response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(downloadFileName, "UTF-8"));
response.setCharacterEncoding("UTF-8");
// 4. 写入响应流,完成下载
try (OutputStream outputStream = response.getOutputStream()) {
template.write(outputStream);
outputStream.flush();
} finally {
// 5. 关闭资源,避免内存泄漏
template.close();
}
}
/**
* 生成Word并保存到本地(可选,根据需求使用)
* @param templateName 模板文件名
* @param data 填充数据
* @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接口,模拟从数据库获取数据(实际开发中替换为真实DAO查询),调用工具类实现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.*;
/**
* 合同导出接口(实战示例)
*/
@RestController
@RequestMapping("/contract")
public class ContractController {
@Resource
private WordGenerateUtil wordGenerateUtil;
/**
* 导出单个客户合同
* @param customerId 客户ID(实际开发中用于查询客户数据)
* @param response 响应对象(返回下载流)
* @throws IOException 异常
*/
@GetMapping("/export/{customerId}")
public void exportContract(@PathVariable String customerId, HttpServletResponse response) throws IOException {
// 1. 模拟从数据库查询客户合同数据(实际开发中替换为真实DAO查询)
ContractDTO contractDTO = getContractData(customerId);
// 2. 组装数据(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. 创建表格数据
RowRenderData row0 = Rows.of("商品名称", "单价(元)","数量","小计(元)").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目录下的图片(实际可从数据库获取图片路径)
data.put("attachmentImg", Pictures.ofStream(
new ClassPathResource("static/img/attachment.jpg").getInputStream(), // 图片流
PictureType.JPEG) // 图片格式,无需手动写后缀
.size(200, 100) // 图片宽高(单位:像素)
.create()
);
// 5. 调用工具类,生成并下载Word
wordGenerateUtil.generateAndDownload(
"contractTemplate.docx", // 模板文件名
data, // 填充数据
response, // 响应对象
contractDTO.getCustomerName() + "的合同.docx" // 下载文件名
);
}
/**
* 模拟查询合同数据(实际开发中替换为真实业务逻辑/DAO查询)
*/
private ContractDTO getContractData(String customerId) {
ContractDTO contract = new ContractDTO();
// 模拟主数据(实际从数据库查询)
contract.setContractNo("HT-" + System.currentTimeMillis());
contract.setCustomerName("张三");
contract.setPhone("13800138000");
contract.setSignDate("2026-03-25");
// 模拟合同明细数据(对应表格循环)
List<ContractDetailDTO> detailList = new ArrayList<>();
ContractDetailDTO detail1 = new ContractDetailDTO();
detail1.setProductName("Java开发服务");
detail1.setPrice(new BigDecimal("5000.00"));
detail1.setNum(1);
detail1.setTotal(new BigDecimal("5000.00"));
ContractDetailDTO detail2 = new ContractDetailDTO();
detail2.setProductName("系统维护服务");
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;
}
}
三、测试验证
测试步骤简单,无需复杂配置,启动SpringBoot项目后,直接访问接口即可验证功能是否正常。
3.1 前置准备
-
确认templates目录下有contractTemplate.docx模板,static/img目录下有attachment.jpg图片;
-
确保项目启动无报错(JDK17环境,依赖正常引入);
-
无需修改application.yml,默认配置即可。
3.2 接口访问与验证
访问接口地址:http://localhost:8080/contract/export/1(customerId随便传,此处仅为模拟),浏览器会自动下载Word文件。

四、常见问题
-
模板后缀必须是.docx,不能是.doc,否则会报"不支持的格式"异常;
-
占位符大小写敏感,比如模板中是{{contractNo}},代码中写contractno会导致填充失败;
-
图片渲染需通过Pictures工具类创建渲染对象,仅传字符串路径会导致图片无法显示;项目内图片用ClassPathResource获取流,本地图片用FileInputStream获取流;
-
批量生成Word时,必须循环关闭template资源,否则会导致内存溢出;
五、扩展场景
实际开发中,除了基础的文本、表格、图片填充,还可能遇到以下场景,简单补充实现思路:
-
条件渲染:某些字段为空时不显示,可使用{{?变量名}} 占位符(如{{?remark}} 备注:{{remark}} {{/?remark}});
-
动态图片:从数据库获取图片流(无需保存到本地),直接传入Pictures.ofStream()方法即可渲染;
-
批量导出:循环调用工具类的generateToLocal方法,生成多个Word文件,再通过ZipOutputStream打包成zip,返回给前端下载。
六、总结
本次实战用SpringBoot3实现Word动态生成与下载,核心逻辑是"模板占位符+数据模型+通用工具类",无需复杂的样式配置,所有代码均可直接复制复用。
对比原生API,这种方式不仅代码简洁,而且后期维护方便------修改模板无需改代码,只需调整Word文件中的占位符和样式即可,极大降低维护成本。