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": "报告导出失败: ..."}
▼
下载失败,前端接收错误提示 ❌