乐于分享
好东西不私藏

SpringBoot3实战:优雅实现Word文档动态生成与下载

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. 1. 模板准备:在src/main/resources目录下,新建templates文件夹,放入Word模板文件(后缀必须是.docx,不能是.doc,否则会报格式错误),命名为contractTemplate.docx;
  2. 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. 1. 确认templates目录下有contractTemplate.docx模板,static/img目录下有attachment.jpg图片;
  2. 2. 确保项目启动无报错(JDK17环境,依赖正常引入);
  3. 3. 无需修改application.yml,默认配置即可。

3.2 接口访问与验证

访问接口地址:http://localhost:8080/contract/export/1(customerId随便传,此处仅为模拟),浏览器会自动下载Word文件。

下载的文件效果如下:

四、常见问题

  1. 1. 模板后缀必须是.docx,不能是.doc,否则会报“不支持的格式”异常;
  2. 2. 占位符大小写敏感,比如模板中是{{contractNo}},代码中写contractno会导致填充失败;
  3. 3. 图片渲染需通过Pictures工具类创建渲染对象,仅传字符串路径会导致图片无法显示;项目内图片用ClassPathResource获取流,本地图片用FileInputStream获取流;
  4. 4. 批量生成Word时,必须循环关闭template资源,否则会导致内存溢出;

五、扩展场景

实际开发中,除了基础的文本、表格、图片填充,还可能遇到以下场景,简单补充实现思路:

  • • 条件渲染:某些字段为空时不显示,可使用{{?变量名}} 占位符(如{{?remark}} 备注:{{remark}} {{/?remark}});
  • • 动态图片:从数据库获取图片流(无需保存到本地),直接传入Pictures.ofStream()方法即可渲染;
  • • 批量导出:循环调用工具类的generateToLocal方法,生成多个Word文件,再通过ZipOutputStream打包成zip,返回给前端下载。

六、总结

本次实战用SpringBoot3实现Word动态生成与下载,核心逻辑是“模板占位符+数据模型+通用工具类”,无需复杂的样式配置,所有代码均可直接复制复用。

对比原生API,这种方式不仅代码简洁,而且后期维护方便——修改模板无需改代码,只需调整Word文件中的占位符和样式即可,极大降低维护成本。

往期推荐

SpringBoot3实战:轻量编排复杂业务,打造可复用组件化流程
SpringBoot3实战:一键搞定多格式文件内容提取
SpringBoot3实战:告别占位符,SQL可视化调试一步到位
SpringBoot3 动态扩展实战:不重启服务,轻松插拔业务模块
SpringBoot3实战:无痛接入云对象存储,搞定文件上传下载