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;

/**
 * 合同主数据模型(对应模板中的普通文本占位符)
 */
@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 前置准备

  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文件中的占位符和样式即可,极大降低维护成本。

相关推荐
AIminminHu2 小时前
OpenGL渲染与几何内核那点事-项目实践理论补充(一-1-(2):看似“老派”的 C++ 底层优化,恰恰是这些前沿领域最需要的基础设施)
开发语言·c++
Eiceblue2 小时前
通过 C# 读取 Word 表格数据:高效解析 + 导出为 CSV/TXT
开发语言·c#·word
小陈工2 小时前
Python开源代码管理避坑实战:从Git高级操作到Docker环境配置
开发语言·git·python·安全·docker·开源·源代码管理
REDcker2 小时前
Java 语言版本演进与特性概要
java·开发语言
励志的小陈2 小时前
C++入门
开发语言·c++
江湖中的阿龙2 小时前
深入理解 CAS:Java 无锁并发核心原理、缺陷与应用场景详解
java·开发语言
进击的荆棘2 小时前
C++起始之路——继承
开发语言·c++
格林威2 小时前
工业相机图像采集处理:从 RAW 数据到 AI 可读图像,堡盟相机 C#实战代码深度解析
c++·人工智能·数码相机·opencv·算法·计算机视觉·c#
NGC_66112 小时前
深入解析 ConcurrentHashMap 设计思想:高并发下的线程安全哈希表
java·开发语言