SpringBoot3实战:优雅实现Word文档动态生成与下载
日常开发中,经常会遇到这样的需求:根据业务数据动态生成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 模板与图片准备
-
1. 模板准备:在src/main/resources目录下,新建templates文件夹,放入Word模板文件(后缀必须是.docx,不能是.doc,否则会报格式错误),命名为contractTemplate.docx; -
2. 图片准备:在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;/** * 合同主数据模型(对应模板中的普通文本占位符) */@Datapublic 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;}/** * 合同明细数据模型(对应表格中的占位符) */@Datapublic 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文档生成工具类(通用,可直接复用) */@Componentpublic 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 前置准备
-
1. 确认templates目录下有contractTemplate.docx模板,static/img目录下有attachment.jpg图片; -
2. 确保项目启动无报错(JDK17环境,依赖正常引入); -
3. 无需修改application.yml,默认配置即可。
3.2 接口访问与验证
访问接口地址:http://localhost:8080/contract/export/1(customerId随便传,此处仅为模拟),浏览器会自动下载Word文件。
下载的文件效果如下:

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