Java 操作 Word:字符串替换、图片插入、动态生成表格与API接口下载

Java 操作 Word:字符串替换、图片插入、动态生成表格与API接口下载


文章目录

  • [Java 操作 Word:字符串替换、图片插入、动态生成表格与API接口下载](#Java 操作 Word:字符串替换、图片插入、动态生成表格与API接口下载)
  • 介绍
  • [1 文本占位符替换](#1 文本占位符替换)
    • [1.1 Maven依赖](#1.1 Maven依赖)
    • [1.2 技术选型](#1.2 技术选型)
    • [1.3 占位符语法详解(poi-tl)](#1.3 占位符语法详解(poi-tl))
    • [1.4 Word 模板内容示例](#1.4 Word 模板内容示例)
    • [1.5 代码](#1.5 代码)
    • [1.6 运行效果](#1.6 运行效果)
  • [2 动态生成单元格插入字符串和图片(com.deepoove+org.apache.poi)](#2 动态生成单元格插入字符串和图片(com.deepoove+org.apache.poi))
    • [2.1 Maven依赖](#2.1 Maven依赖)
    • [2.2 技术选型说明](#2.2 技术选型说明)
    • [2.3 程序思路](#2.3 程序思路)
    • [2.4 代码](#2.4 代码)
    • [2.5 运行效果](#2.5 运行效果)
  • [3 动态生成单元格插入字符串和图片(org.apache.poi)](#3 动态生成单元格插入字符串和图片(org.apache.poi))
    • [3.1 Maven依赖](#3.1 Maven依赖)
    • [3.2 技术选型说明](#3.2 技术选型说明)
    • [3.3 代码](#3.3 代码)
  • [4 API接口下载Word](#4 API接口下载Word)
    • [4.1 API接口代码方法](#4.1 API接口代码方法)
    • [4.2 返回Word对象代码方法](#4.2 返回Word对象代码方法)

介绍

在日常的报告、合同、通告生成中,Word 文档的自动化填充尤为关键。通过 Java + Apache POI 、我们可以轻松实现以下功能:

  • 替换 Word 模板中的占位符
  • 插入本地或网络图片
  • 构建动态表格(含字段与图片)

1 文本占位符替换

1.1 Maven依赖

xml 复制代码
<dependencies>
    <!--
      poi-tl 是基于 Apache POI 封装的 Word 模板引擎,用于生成 DOCX 报告。
      支持文本替换、图片插入、表格循环、条件判断等高级模板功能。
      官网:https://github.com/Sayi/poi-tl
    -->
  <dependency>
    <groupId>com.deepoove</groupId>
    <artifactId>poi-tl</artifactId>
    <version>1.12.0</version>
  </dependency>
</dependencies>

1.2 技术选型

模块 用途 版本 Java 8 支持
poi-tl Word 模板占位符替换 1.12.0

1.3 占位符语法详解(poi-tl)

占位符类型 语法形式 描述 示例
文本占位符 {``{field}} 替换为普通字符串 {``{taskName}}
图片占位符 {``{@field}} 替换为图片(支持本地/网络/byte\[\]) {``{@image}}
数字格式化 {``{field?###}} 按指定格式输出数字 {``{score?0.00}}
日期格式化 {``{field?date('yyyy-MM-dd')}} 格式化 java.util.Date 对象 {``{createTime?date('yyyy-MM-dd')}}
逻辑判断 {``{if(condition)}}...{``{/if}} 条件渲染 {``{if(imageCount > 0)}}...{``{/if}}
列表渲染 {``{list}} + {``{each items item}}...{``{/each}} 渲染列表型数据(List、数组等) {``{each items item}}...{``{/each}}

1.4 Word 模板内容示例

下面只用到啦字符串和图片

模版下载地址:https://download.csdn.net/download/EXIxiaozhou/90959860

1.5 代码

java 复制代码
package org.example.xxxx;

import com.deepoove.poi.XWPFTemplate;
import com.deepoove.poi.config.Configure;
import com.deepoove.poi.data.PictureRenderData;
import com.deepoove.poi.data.Pictures;

import java.io.File;
import java.io.FileOutputStream;
import java.util.*;

/**
 * 巡检报告生成器
 * 使用 poi-tl 模板引擎将 Word 模板渲染为最终报告
 */
public class PatrolReportGenerator {

    // === 模板与输出路径常量 ===
    private static final String TEMPLATE_PATH = "src/main/resources/templates/示例模版.docx";
    private static final String OUTPUT_PATH = "output/巡检报告.docx";
    private static final String IMAGE_PATH = "src/main/resources/images/img.jpg";
    private static final String HTTPS_IMAGE_PATH = "https://xxx/img.jpg";

    public static void main(String[] args) {
        try {
            // ✅ 填充文本内容数据
            Map<String, Object> data = new HashMap<>();
            data.put("index", "1");
            data.put("time", "2024-09-11 00:00:00");
            data.put("longitude", "120.5xxxxxx");
            data.put("latitude", "29.9xxxxxx");
            data.put("eventType", "xxx");
            data.put("location", "湖北武汉xxx");

            // ✅ 插入图片(px宽高:建议按 96DPI 测算)// 本地加载 Pictures.ofLocal
            // PictureRenderData picture = Pictures.ofLocal(IMAGE_PATH)
            //         .size(510, 280) // 宽高 px
            //         .create();

            // Https
            PictureRenderData picture = Pictures.ofUrl(HTTPS_IMAGE_PATH)
                    .size(510, 280) // 宽高 px
                    .create();
            data.put("alarmImage", picture);

            // ✅ 输出文件夹准备
            File outDir = new File("output");
            if (!outDir.exists() && !outDir.mkdirs()) {
                throw new RuntimeException("❌ 无法创建输出目录:" + outDir.getAbsolutePath());
            }
            // ✅ 渲染模板并导出
            XWPFTemplate template = XWPFTemplate.compile(TEMPLATE_PATH).render(data);
            try (FileOutputStream out = new FileOutputStream(OUTPUT_PATH)) {
                template.write(out);
                template.close();
            }

            System.out.println("✅ 巡检报告生成成功:" + OUTPUT_PATH);

        } catch (Exception e) {
            System.err.println("❌ 报告生成失败:" + e.getMessage());
            e.printStackTrace();
        }
    }
}

1.6 运行效果

2 动态生成单元格插入字符串和图片(com.deepoove+org.apache.poi)

2.1 Maven依赖

xml 复制代码
<dependencies>
    <!--
      poi-tl 是基于 Apache POI 封装的 Word 模板引擎,用于生成 DOCX 报告。
      支持文本替换、图片插入、表格循环、条件判断等高级模板功能。
      官网:https://github.com/Sayi/poi-tl
    -->
  <dependency>
    <groupId>com.deepoove</groupId>
    <artifactId>poi-tl</artifactId>
    <version>1.12.0</version>
  </dependency>
  <!-- 核心功能支持:读取处理 Excel、Word、PPT 等 -->
  <dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi</artifactId>
    <version>5.2.5</version>
  </dependency>

  <!-- OOXML 格式支持:处理 .docx、.xlsx、.pptx 等 -->
  <dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-ooxml</artifactId>
    <version>5.2.5</version>
  </dependency>
</dependencies>

2.2 技术选型说明

模块名称 主要用途 版本 Java 8 支持 是否必须
poi-tl Word 模板占位符替换 1.12.0 ✅ 必需
poi Apache POI 核心模块,支持 XLS、DOC 格式 5.2.5 ✅ 必需
poi-ooxml 支持 Office OpenXML 格式,如 DOCX、XLSX 5.2.5 ✅ 必需

2.3 程序思路

┌────────────────────────────────────────────────────────────┐

│ 【Step 1】加载 Word 模板并渲染文本变量 │

├────────────────────────────────────────────────────────────┤

│ 使用 poi-tl 模板引擎加载 Word 模板(.docx) │

│ 替换模板中 {变量名} 格式的文本(例如 {taskName}) │

│ → 得到渲染后的 XWPFDocument 文档对象 │

└────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────┐

│ 【Step 2】创建空表格并清空默认行 │

├────────────────────────────────────────────────────────────┤

│ 调用 doc.createTable(1, 1) 创建初始 1x1 表格 │

│ 删除默认第一行:table.removeRow(0)

│ 准备添加自定义标题和内容 │

└────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────┐

│ 【Step 3】添加表头并设置样式 │

├────────────────────────────────────────────────────────────┤

│ 创建表头行:XWPFTableRow headerRow = table.createRow();

│ 遍历 titleList 数组添加列标题(如"时间"、"位置"等) │

│ 设置每列宽度、单元格水平垂直居中 │

│ → 使用 setHeaderRowStyle() 方法封装 │

└────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────┐

│ 【Step 4】循环遍历数据,插入数据和图片 │

├────────────────────────────────────────────────────────────┤

│ for (dataRow in dataRows): │

│ ┌── 插入一行表格数据: │

│ │ - 每列设置文本、列宽 │

│ │ - 使用 setCellWidth() 设置宽度 │

│ └── │

│ ┌── 插入对应图片行: │

│ │ - 新建一行 row │

│ │ - 合并整行所有列(setGridSpan) │

│ │ - 插入段落(XWPFParagraph)并居中 │

│ │ - 使用 imageRun.addPicture() 插入图片(本地或网络) │

│ │ - 设置大小为 EMU 单位:410x220 px │

│ └── 使用 insertImageRow() 方法封装 │

└────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────┐

│ 【Step 5】输出最终 Word 文件 │

├────────────────────────────────────────────────────────────┤

│ 使用 doc.write(FileOutputStream) 输出为 .docx 文件 │

│ 文件路径:output/巡检报告.docx │

│ 完成:结构化表格 + 图片嵌入 + 文本渲染 │

└────────────────────────────────────────────────────────────┘

2.4 代码

java 复制代码
package org.example.xxxx;

import com.deepoove.poi.XWPFTemplate;
import org.apache.poi.util.Units;
import org.apache.poi.xwpf.usermodel.*;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.*;

import org.apache.poi.util.Units;
import org.apache.poi.xwpf.usermodel.XWPFRun;
import org.apache.poi.xwpf.usermodel.Document;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.net.URL;
import java.util.Map;


public class test4 {

    // 模板路径
    private static final String TEMPLATE_PATH = "src/main/resources/templates/示例模版.docx";

    // 标题字段列表
    private static final String[] titleList = new String[]{
            "序号", "时间", "经度", "纬度", "类型", "位置"
    };

    // 每列宽度,单位:twip(1 cm ≈ 567 twip)
    private static final int[] colWidths = new int[]{
            1500, 1600, 1600, 1600, 1600, 4000
    };

    /**
     * 设置标题行样式(水平居中、垂直居中、列宽)
     *
     * @param row       标题行对象
     * @param titles    每列标题
     * @param colWidths 每列宽度,单位twip
     */
    private static void setHeaderRowStyle(XWPFTableRow row, String[] titles, int[] colWidths) {
        for (int j = 0; j < titles.length; j++) {
            /**
             * 如果是第0列,获取已有的单元格;否则为当前行新增一个单元格
             * 默认只生成第一列单元格(即 cell[0])。其他单元格需要你手动添加,否则会报空指针或写不进去
             */
            XWPFTableCell cell;
            if (row.getTableCells().size() > j) {
                // 已存在:复用 cell
                cell = row.getCell(j);
            } else {
                // 不存在:创建新 cell
                cell = row.createCell();
            }
            cell.setText(titles[j]);
            // 水平居中
            cell.getParagraphs().get(0).setAlignment(ParagraphAlignment.CENTER);
            // 垂直居中
            CTTcPr tcPr = cell.getCTTc().isSetTcPr() ? cell.getCTTc().getTcPr() : cell.getCTTc().addNewTcPr();
            CTVerticalJc va = tcPr.isSetVAlign() ? tcPr.getVAlign() : tcPr.addNewVAlign();
            va.setVal(STVerticalJc.CENTER);
            // 设置列宽
            CTTblWidth tcW = tcPr.isSetTcW() ? tcPr.getTcW() : tcPr.addNewTcW();
            tcW.setW(BigInteger.valueOf(colWidths[j]));
            tcW.setType(STTblWidth.DXA);
        }
    }

    /**
     * 设置普通单元格的列宽(单位为 twip,1 cm ≈ 567 twip)
     *
     * @param cell  目标单元格 XWPFTableCell
     * @param width 单元格列宽(单位:twip)
     *
     * 说明:
     * Word 的列宽设置不是直接设置像素或厘米,而是设置 twip(twentieth of a point),
     * 1 inch = 1440 twip,1 cm ≈ 567 twip。
     *
     * 所以:
     * - 567 = 1cm
     * - 1134 = 2cm
     * - 1700 ≈ 3cm
     */
    private static void setCellWidth(XWPFTableCell cell, int width) {
        // ① 获取该单元格的 XML 底层属性对象 <w:tcPr>(即 TableCell Properties)
        // 若已存在就复用,否则创建新的
        CTTcPr tcPr = cell.getCTTc().isSetTcPr()
                ? cell.getCTTc().getTcPr()
                : cell.getCTTc().addNewTcPr();

        // ② 获取或创建单元格宽度元素 <w:tcW>
        CTTblWidth tcW = tcPr.isSetTcW()
                ? tcPr.getTcW()
                : tcPr.addNewTcW();

        // ③ 设置宽度值
        tcW.setW(BigInteger.valueOf(width));  // 设置列宽(单位:twip)

        // ④ 设置宽度类型为 DXA(twip 单位),还有其他类型比如 AUTO、PCT(百分比)等
        tcW.setType(STTblWidth.DXA);  // DXA = "twip"
    }

    /**
     * 根据文件路径自动判断 Word 支持的图片类型
     */
    private static int getPictureType(String imagePath) {
        String lowerPath = imagePath.toLowerCase();

        if (lowerPath.endsWith(".png")) return Document.PICTURE_TYPE_PNG;
        if (lowerPath.endsWith(".jpg") || lowerPath.endsWith(".jpeg")) return Document.PICTURE_TYPE_JPEG;
        if (lowerPath.endsWith(".gif")) return Document.PICTURE_TYPE_GIF;
        if (lowerPath.endsWith(".bmp")) return Document.PICTURE_TYPE_BMP;
        if (lowerPath.endsWith(".wmf")) return Document.PICTURE_TYPE_WMF;
        if (lowerPath.endsWith(".emf")) return Document.PICTURE_TYPE_EMF;

        // 默认 PNG(也可以抛出异常)
        return Document.PICTURE_TYPE_PNG;
    }

    /**
     * 插入一行图片到 Word 表格中(合并整行单元格并居中显示图片)
     *
     * @param table         Word 表格对象
     * @param colCount      当前表格的总列数(用于合并)
     * @param widthsSum 合并后的总宽度(单位 twip)
     * @param imagePath     本地图片路径
     */
    private static void insertImageRow(XWPFTable table, int colCount, int widthsSum, String imagePath) throws Exception {
        // ① 创建一行
        XWPFTableRow imageRow = table.createRow();

        // ② 删除除第 0 列之外的其他列(彻底移除结构)
        while (imageRow.getTableCells().size() > 1) {
            imageRow.removeCell(1);
        }

        // ③ 获取合并单元格
        XWPFTableCell cell = imageRow.getCell(0);

        // 设置合并单元格样式
        CTTcPr tcPr = cell.getCTTc().isSetTcPr() ? cell.getCTTc().getTcPr() : cell.getCTTc().addNewTcPr();

        // 设置 GridSpan(合并列)
        tcPr.addNewGridSpan().setVal(BigInteger.valueOf(colCount));

        // 设置宽度为整行宽
        CTTblWidth tblWidth = tcPr.isSetTcW() ? tcPr.getTcW() : tcPr.addNewTcW();
        tblWidth.setW(BigInteger.valueOf(widthsSum));
        tblWidth.setType(STTblWidth.DXA);

        // 插入段落和图片
        XWPFParagraph picPara = cell.addParagraph();
        picPara.setAlignment(ParagraphAlignment.CENTER);
        XWPFRun imageRun = picPara.createRun();

        // ⑤ 插入图片(根据像素宽高换算 EMU 单位)本地
        try {
            InputStream is;
            if (imagePath.contains("http")) {
                is = new URL(imagePath).openStream();  // 网络图片
            } else {
                is = new FileInputStream(imagePath);  // 本地图片
            }
            imageRun.addPicture(
                    is,
                    getPictureType(imagePath), // 你也可以根据扩展名动态判断类型
                    imagePath,
                    Units.toEMU(410),
                    Units.toEMU(220)
            );
        } catch (Exception e) {
            System.out.println("图片插入失败:" + e.toString());
        }
    }

    /**
     * 向 Word 文档中插入带有标题、数据和图片的表格
     *
     * @param doc        Word 文档对象
     * @param dataRows   表格数据,每个元素为一行
     * @param imagePaths  插入图片的路径列表
     */
    public static void generateTableWithImages(XWPFDocument doc, List<String[]> dataRows, String[] imagePaths) {
        int totalWidth = Arrays.stream(colWidths).sum();
        // === 创建空表格(1行1列),再清除初始化行 ===
        XWPFTable table = doc.createTable(1, 1);
        table.removeRow(0); // 删除默认第一行
        XWPFTableRow headerRow = table.createRow();
        setHeaderRowStyle(headerRow, titleList, colWidths);  // 创建表格标题

        // 遍历每一行数据 进行表格字段值插入
        for (int i = 0; i < dataRows.size(); i++) {
            String[] rowData = dataRows.get(i);  // 获取当前坐标的表格数据

            // 给一行的表格提填充数据
            XWPFTableRow row = table.createRow();
            for (int j = 0; j < rowData.length; j++) {
                XWPFTableCell cell;
                // 若当前行已有该列的单元格(通常只会有 cell[0]),则获取该单元格
                if (row.getTableCells().size() > j) {
                    cell = row.getCell(j); // 复用已存在的单元格
                } else {
                    // 否则说明单元格尚未创建,需手动创建新单元格
                    cell = row.createCell(); // 动态添加新的列单元格
                }
                cell.setText(rowData[j]);  // 给单元格设置内容
                setCellWidth(cell, colWidths[j]);  // 给单元格设置宽度
            }
            // 在字段值下面新建一行单元格合并后插入一张图片并设置大小
            try {
                String imagePath = imagePaths[i];
                insertImageRow(table, titleList.length, totalWidth, imagePath);
            } catch (Exception e) {
                System.err.println("⚠️ 插入图片失败:" + e.getMessage());
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        try  {
            Map<String, Object> data = new HashMap<>();
            data.put("index", "1");
            data.put("time", "2024-09-11 00:00:00");
            data.put("longitude", "120.5xxxxxx");
            data.put("latitude", "29.9xxxxxx");
            data.put("eventType", "xxx");
            data.put("location", "深圳xxx");

            XWPFTemplate template = XWPFTemplate.compile(TEMPLATE_PATH).render(data);
            XWPFDocument doc = template.getXWPFDocument();

            // 表格数据(第0行是标题)
            List<String[]> dataRows = Arrays.asList(
                    new String[]{"1", "2025-06-01 10:00", "120.5xxxxxx", "29.9xxxxxx" , "xxx", "深圳xxx"},
                    new String[]{"2", "2025-06-01 10:10", "120.5xxxxxx", "29.9xxxxxx", "xxx", "深圳xxx"},
                    new String[]{"3", "2025-06-06 10:47", "120.5xxxxxx", "29.9xxxxxx", "xxx", "深圳xxx"}
            );

            String[] httpsImagePaths = new String[]{
                    "https://xxxx/img.png",
                    "https://xxxx/img.png",
                    "https://xxxx/img.png"
            };

            // 输出路径
            String OUTPUT_PATH = "output/巡检报告.docx";

            // 调用表格生成数据、图片插入方法
            generateTableWithImages(doc, dataRows, httpsImagePaths);

            // 输出word文件
            try (FileOutputStream out = new FileOutputStream(OUTPUT_PATH)) {
                doc.write(out);
            }
            System.out.println("✅ 表格与图片已成功插入 Word 文件:" + OUTPUT_PATH);

        } catch (Exception e) {
            System.err.println("❌ 错误:" + e.getMessage());
            e.printStackTrace();
        }
    }
}

2.5 运行效果

3 动态生成单元格插入字符串和图片(org.apache.poi)

3.1 Maven依赖

xml 复制代码
<dependencies>
  <!-- Apache POI 主库:处理 Word、Excel、PPT 等文档的核心功能 -->
  <dependency>
      <groupId>org.apache.poi</groupId>
      <artifactId>poi</artifactId>
      <version>5.2.5</version>
  </dependency>

  <!-- Apache POI OOXML:用于解析 .docx、.xlsx 等基于 Office Open XML 的格式 -->
  <dependency>
      <groupId>org.apache.poi</groupId>
      <artifactId>poi-ooxml</artifactId>
      <version>5.2.5</version>
  </dependency>
  
  <!-- ✅ 可选:支持 .docx 内部复杂结构,POI 5.x 所需 -->
  <dependency>
    <groupId>org.apache.xmlbeans</groupId>
    <artifactId>xmlbeans</artifactId>
    <version>5.1.1</version>
  </dependency>

  <!-- ✅ 可选:处理某些 Word 模板中使用的 schemas 类型 -->
  <dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-ooxml-schemas</artifactId>
    <version>4.1.2</version> <!-- POI 官方建议使用该版本 -->
  </dependency>

  <!-- ✅ 可选:文件流操作工具(复制、下载、字节流等) -->
  <dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.13.0</version>
  </dependency>
</dependencies>

3.2 技术选型说明

模块名称 主要用途 版本 Java 8 支持 是否必须
poi Apache POI 核心模块,支持 XLS、DOC 格式 5.2.5 ✅ 必需
poi-ooxml 支持 Office OpenXML 格式,如 DOCX、XLSX 5.2.5 ✅ 必需
xmlbeans POI 5.x 的底层依赖,用于解析复杂 XML 结构 5.1.1 ⚠️ 建议添加(模板类文档可能需要)
poi-ooxml-schemas 支持一些复杂 DOCX/Excel 模板内建 schemas 4.1.2 ⚠️ 建议添加(不添加可能 NPE)
commons-io 文件、流操作工具类,简化图片插入等操作 2.13.0 ⚠️ 建议添加(处理 InputStream 更方便)

3.3 代码

java 复制代码
package org.example.xxxx;

import org.apache.poi.util.Units;
import org.apache.poi.xwpf.usermodel.*;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.*;

import org.apache.poi.xwpf.usermodel.XWPFRun;
import org.apache.poi.xwpf.usermodel.Document;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.net.URL;
import java.util.Map;


public class test5 {

    // 模板路径
    private static final String TEMPLATE_PATH = "src/main/resources/templates/示例模版.docx";

    // 标题字段列表
    private static final String[] titleList = new String[]{
            "序号", "时间", "经度", "纬度", "类型", "位置"
    };

    // 每列宽度,单位:twip(1 cm ≈ 567 twip)
    private static final int[] colWidths = new int[]{
            1500, 1600, 1600, 1600, 1600, 4000
    };

    /**
     * 设置标题行样式(水平居中、垂直居中、列宽)
     *
     * @param row       标题行对象
     * @param titles    每列标题
     * @param colWidths 每列宽度,单位twip
     */
    private static void setHeaderRowStyle(XWPFTableRow row, String[] titles, int[] colWidths) {
        for (int j = 0; j < titles.length; j++) {
            /**
             * 如果是第0列,获取已有的单元格;否则为当前行新增一个单元格
             * 默认只生成第一列单元格(即 cell[0])。其他单元格需要你手动添加,否则会报空指针或写不进去
             */
            XWPFTableCell cell;
            if (row.getTableCells().size() > j) {
                // 已存在:复用 cell
                cell = row.getCell(j);
            } else {
                // 不存在:创建新 cell
                cell = row.createCell();
            }
            cell.setText(titles[j]);
            // 水平居中
            cell.getParagraphs().get(0).setAlignment(ParagraphAlignment.CENTER);
            // 垂直居中
            CTTcPr tcPr = cell.getCTTc().isSetTcPr() ? cell.getCTTc().getTcPr() : cell.getCTTc().addNewTcPr();
            CTVerticalJc va = tcPr.isSetVAlign() ? tcPr.getVAlign() : tcPr.addNewVAlign();
            va.setVal(STVerticalJc.CENTER);
            // 设置列宽
            CTTblWidth tcW = tcPr.isSetTcW() ? tcPr.getTcW() : tcPr.addNewTcW();
            tcW.setW(BigInteger.valueOf(colWidths[j]));
            tcW.setType(STTblWidth.DXA);
        }
    }

    /**
     * 设置普通单元格的列宽(单位为 twip,1 cm ≈ 567 twip)
     *
     * @param cell  目标单元格 XWPFTableCell
     * @param width 单元格列宽(单位:twip)
     *
     * 说明:
     * Word 的列宽设置不是直接设置像素或厘米,而是设置 twip(twentieth of a point),
     * 1 inch = 1440 twip,1 cm ≈ 567 twip。
     *
     * 所以:
     * - 567 = 1cm
     * - 1134 = 2cm
     * - 1700 ≈ 3cm
     */
    private static void setCellWidth(XWPFTableCell cell, int width) {
        // ① 获取该单元格的 XML 底层属性对象 <w:tcPr>(即 TableCell Properties)
        // 若已存在就复用,否则创建新的
        CTTcPr tcPr = cell.getCTTc().isSetTcPr()
                ? cell.getCTTc().getTcPr()
                : cell.getCTTc().addNewTcPr();

        // ② 获取或创建单元格宽度元素 <w:tcW>
        CTTblWidth tcW = tcPr.isSetTcW()
                ? tcPr.getTcW()
                : tcPr.addNewTcW();

        // ③ 设置宽度值
        tcW.setW(BigInteger.valueOf(width));  // 设置列宽(单位:twip)

        // ④ 设置宽度类型为 DXA(twip 单位),还有其他类型比如 AUTO、PCT(百分比)等
        tcW.setType(STTblWidth.DXA);  // DXA = "twip"
    }

    /**
     * 根据文件路径自动判断 Word 支持的图片类型
     */
    private static int getPictureType(String imagePath) {
        String lowerPath = imagePath.toLowerCase();

        if (lowerPath.endsWith(".png")) return Document.PICTURE_TYPE_PNG;
        if (lowerPath.endsWith(".jpg") || lowerPath.endsWith(".jpeg")) return Document.PICTURE_TYPE_JPEG;
        if (lowerPath.endsWith(".gif")) return Document.PICTURE_TYPE_GIF;
        if (lowerPath.endsWith(".bmp")) return Document.PICTURE_TYPE_BMP;
        if (lowerPath.endsWith(".wmf")) return Document.PICTURE_TYPE_WMF;
        if (lowerPath.endsWith(".emf")) return Document.PICTURE_TYPE_EMF;

        // 默认 PNG(也可以抛出异常)
        return Document.PICTURE_TYPE_PNG;
    }

    /**
     * 插入一行图片到 Word 表格中(合并整行单元格并居中显示图片)
     *
     * @param table         Word 表格对象
     * @param colCount      当前表格的总列数(用于合并)
     * @param widthsSum 合并后的总宽度(单位 twip)
     * @param imagePath     本地图片路径
     */
    private static void insertImageRow(XWPFTable table, int colCount, int widthsSum, String imagePath) throws Exception {
        // ① 创建一行
        XWPFTableRow imageRow = table.createRow();

        // ② 删除除第 0 列之外的其他列(彻底移除结构)
        while (imageRow.getTableCells().size() > 1) {
            imageRow.removeCell(1);
        }

        // ③ 获取合并单元格
        XWPFTableCell cell = imageRow.getCell(0);

        // 设置合并单元格样式
        CTTcPr tcPr = cell.getCTTc().isSetTcPr() ? cell.getCTTc().getTcPr() : cell.getCTTc().addNewTcPr();

        // 设置 GridSpan(合并列)
        tcPr.addNewGridSpan().setVal(BigInteger.valueOf(colCount));

        // 设置宽度为整行宽
        CTTblWidth tblWidth = tcPr.isSetTcW() ? tcPr.getTcW() : tcPr.addNewTcW();
        tblWidth.setW(BigInteger.valueOf(widthsSum));
        tblWidth.setType(STTblWidth.DXA);

        // 插入段落和图片
        XWPFParagraph picPara = cell.addParagraph();
        picPara.setAlignment(ParagraphAlignment.CENTER);
        XWPFRun imageRun = picPara.createRun();

        // ⑤ 插入图片(根据像素宽高换算 EMU 单位)本地
        try {
            InputStream is;
            if (imagePath.contains("http")) {
                is = new URL(imagePath).openStream();  // 网络图片
            } else {
                is = new FileInputStream(imagePath);  // 本地图片
            }
            imageRun.addPicture(
                    is,
                    getPictureType(imagePath), // 你也可以根据扩展名动态判断类型
                    imagePath,
                    Units.toEMU(410),
                    Units.toEMU(220)
            );
        } catch (Exception e) {
            System.out.println("图片插入失败:" + e.toString());
        }
    }

    /**
     * 向 Word 文档中插入带有标题、数据和图片的表格
     *
     * @param doc        Word 文档对象
     * @param dataRows   表格数据,每个元素为一行
     * @param imagePaths  插入图片的路径列表
     */
    public static void generateTableWithImages(XWPFDocument doc, List<String[]> dataRows, String[] imagePaths) {
        int totalWidth = Arrays.stream(colWidths).sum();
        // === 创建空表格(1行1列),再清除初始化行 ===
        XWPFTable table = doc.createTable(1, 1);
        table.removeRow(0); // 删除默认第一行
        XWPFTableRow headerRow = table.createRow();
        setHeaderRowStyle(headerRow, titleList, colWidths);  // 创建表格标题

        // 遍历每一行数据 进行表格字段值插入
        for (int i = 0; i < dataRows.size(); i++) {
            String[] rowData = dataRows.get(i);  // 获取当前坐标的表格数据

            // 给一行的表格提填充数据
            XWPFTableRow row = table.createRow();
            for (int j = 0; j < rowData.length; j++) {
                XWPFTableCell cell;
                // 若当前行已有该列的单元格(通常只会有 cell[0]),则获取该单元格
                if (row.getTableCells().size() > j) {
                    cell = row.getCell(j); // 复用已存在的单元格
                } else {
                    // 否则说明单元格尚未创建,需手动创建新单元格
                    cell = row.createCell(); // 动态添加新的列单元格
                }
                cell.setText(rowData[j]);  // 给单元格设置内容
                setCellWidth(cell, colWidths[j]);  // 给单元格设置宽度
            }
            // 在字段值下面新建一行单元格合并后插入一张图片并设置大小
            try {
                String imagePath = imagePaths[i];
                insertImageRow(table, titleList.length, totalWidth, imagePath);
            } catch (Exception e) {
                System.err.println("⚠️ 插入图片失败:" + e.getMessage());
                e.printStackTrace();
            }
        }
    }

    /**
     * 替换 Word 中段落或表格单元格内的变量文本。
     * 例如:{{taskId}} 替换为对应的真实值。
     *
     * 注意:该方法只处理在同一个 XWPFRun 中的变量,如果变量被拆分为多个 run,将无法正确替换。
     *
     * @param runs 段落或单元格中的 XWPFRun 列表
     * @param data 替换数据,键为变量名(不含大括号),值为替换内容
     */
    private static void replaceTextInRuns(List<XWPFRun> runs, Map<String, String> data) {
        if (runs == null) return; // 空列表直接返回,避免空指针

        for (XWPFRun run : runs) {
            String text = run.getText(0); // 读取 run 中的文本(索引为 0)

            if (text != null) {
                // 遍历所有要替换的数据项,例如 taskId -> "20250328"
                for (Map.Entry<String, String> entry : data.entrySet()) {
                    // 构造变量占位符,例如 {{taskId}}
                    String key = "{{" + entry.getKey() + "}}";

                    // 如果当前 run 的文本中包含这个占位符,就替换它
                    if (text.contains(key)) {
                        text = text.replace(key, entry.getValue()); // 执行替换
                    }
                }

                // 将替换后的文本重新设置回该 run 中(替换 index 0 的文本)
                run.setText(text, 0);
            }
        }
    }


    public static void main(String[] args) {
        try  {
            Map<String, String> data = new HashMap<>();
            data.put("index", "1");
            data.put("time", "2024-09-11 00:00:00");
            data.put("longitude", "120.5xxxxxx");
            data.put("latitude", "29.9xxxxxx");
            data.put("eventType", "xxx");
            data.put("location", "深圳xxx");
            
            try (FileInputStream fis = new FileInputStream(TEMPLATE_PATH);
                 XWPFDocument doc = new XWPFDocument(fis)) {

                // 替换段落中的变量
                for (XWPFParagraph paragraph : doc.getParagraphs()) {
                    replaceTextInRuns(paragraph.getRuns(), data);
                }

                // 替换表格中的变量
                for (XWPFTable table : doc.getTables()) {
                    for (XWPFTableRow row : table.getRows()) {
                        for (XWPFTableCell cell : row.getTableCells()) {
                            for (XWPFParagraph para : cell.getParagraphs()) {
                                replaceTextInRuns(para.getRuns(), data);
                            }
                        }
                    }
                }

                // 表格数据(第0行是标题)
                List<String[]> dataRows = Arrays.asList(
                        new String[]{"1", "2025-06-01 10:00", "120.5xxxxxx", "29.9xxxxxx" , "xxx", "深圳xxx"},
                        new String[]{"2", "2025-06-01 10:10", "120.5xxxxxx", "29.9xxxxxx", "xxx", "深圳xxx"},
                        new String[]{"3", "2025-06-06 10:47", "120.5xxxxxx", "29.9xxxxxx", "xxx", "深圳xxx"}
                );

                String[] httpsImagePaths = new String[]{
                        "https://xxxx/img.png",
                        "https://xxxx/img.png",
                        "https://xxxx/img.png"
                };
                // 输出路径
                String OUTPUT_PATH = "output/巡检报告.docx";

                generateTableWithImages(doc, dataRows, httpsImagePaths);

                // 输出word文件
                try (FileOutputStream out = new FileOutputStream(OUTPUT_PATH)) {
                    doc.write(out);
                }
                System.out.println("✅ 表格与图片已成功插入 Word 文件:" + OUTPUT_PATH);
            }
        } catch (Exception e) {
            System.err.println("❌ 错误:" + e.getMessage());
            e.printStackTrace();
        }
    }
}

4 API接口下载Word

4.1 API接口代码方法

java 复制代码
    @PostMapping("report/download")
    @ResponseBody
    @CrossOrigin
    public void eventAlarmList(@RequestHeader(name = "token") String token, @RequestBody Map<String, Object> map, HttpServletResponse response) {
        try {
            XWPFDocument document = new ReportGeneratorUtils().runs();

            String wordFileName = "报告.docx";
            String encodedFileName = URLEncoder.encode(wordFileName, "UTF-8").replaceAll("\\+", "%20");
            // 设置响应头,兼容不同浏览器
            String contentDisposition = String.format(
                    "attachment; filename=\"%s\"; filename*=UTF-8''%s", encodedFileName, encodedFileName
            );
            response.setHeader("Content-Disposition", contentDisposition);
            response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
            response.setCharacterEncoding("UTF-8");
            document.write(response.getOutputStream());
            document.close();
        } catch (Exception e) {
            try {
                log.error("巡检报告下载-构建失败:{}", e.getMessage());
                response.reset();
                response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
                response.setContentType("application/json;charset=UTF-8");
                response.getWriter().write("{\"message\":\"报告导出失败: " + e.getMessage() + "\"}");
            } catch (Exception ex) {
                log.error("巡检报告下载-导出失败响应体设置异常:{}", ex.getMessage());
                ex.printStackTrace();
            }
        }

4.2 返回Word对象代码方法

java 复制代码
@Slf4j
public class ReportGeneratorUtils {
    public XWPFDocument runs() {
        try (FileInputStream fis = new FileInputStream(TEMPLATE_PATH)) {
            XWPFDocument doc = new XWPFDocument(fis);
            generateTableWithImages(doc, dataRows, httpsImagePaths);
            return doc;
        } catch (Exception e) {
            log.error("巡检报告生成-失败 任务ID:{}, 错误消息:{}", missionRecordsVo.getMissionId(), e.toString());
            return null;
        }
    }
}

┌─────────────────────────────┐

│ 前端发起请求 │

│ POST /report/download │

└─────────────┬───────────────┘

┌────────────────────────────────────┐

│ Controller 方法 report/download │

└─────────────┬──────────────────────┘

├── 读取请求头 token

├── 解析请求体 map(任务ID等)

┌────────────────────────────────────┐

│ 调用 ReportGeneratorUtils.runs() │

└─────────────┬──────────────────────┘

├── 读取 Word 模板(XWPFDocument)

├── 插入表格与图片(generateTableWithImages)

└── 返回生成好的 Word 文档对象

┌────────────────────────────────────┐

│ 设置 HTTP 响应头 │

└─────────────┬──────────────────────┘

├── Content-Disposition:attachment

├── 文件名 URL 编码(中文兼容)

└── Content-Type:Word MIME 类型

┌────────────────────────────────────┐

│ 将 Word 文档写入 response 输出流 │

└─────────────┬──────────────────────┘

└── 浏览器弹出"文件下载保存"对话框

下载成功,流程结束 ✅

───────────────────────────────────────

复制代码
 ⇩ 若发生异常 ⇩

┌────────────────────────────────────┐

│ 异常处理流程 │

└─────────────┬──────────────────────┘

├── 记录日志(log.error)

├── 设置 response 状态为 500

└── 返回 JSON:{"message": "报告导出失败: ..."}

下载失败,前端接收错误提示 ❌

相关推荐
会周易的程序员1 小时前
给 PLC 写一个字节码虚拟机:STVM 虚拟机架构设计
开发语言·c++·虚拟机·软plc·iec61131·stvm
机器视觉知识推荐、就业指导1 小时前
为什么老说“纯 Qt 没啥就业市场”?
开发语言·qt
telepan1 小时前
Qt 开发避坑与性能指南:如何优雅、安全地定义全局常量字符串?
开发语言·qt
m0_695251491 小时前
Qt MaintenanceTool 使用国内镜像加速下载(超详细教程)
开发语言·qt
2601_961901701 小时前
SpringBoot使用Nacos进行application.yml配置管理
java·spring boot·spring
何以解忧,唯有..2 小时前
LangChain 工具调用(Tool Calling)实战指南
java·前端·langchain
大衛說2 小时前
《Python 从入门到精通》系列总览与学习路线
开发语言·python·学习
QQ_21696290963 小时前
【源码编号:project79475】SpringBoot校内二手交易平台:商品发布、分类检索、留言交流、订单管理全流程实战
java·spring boot·后端
可乐鸡翅yeah_4 小时前
hls.js 内存泄漏实战排查,直播 M3U8 长时间播放异常定位方案
开发语言·javascript·python·django·ecmascript·m3u8·m3u8在线