前言
在日常开发中,我们经常需要动态生成 Word 文档------无论是生成报表、合同、还是导出各类业务数据。Apache POI 的 XWPF 模块是 Java 生态中操作 .docx 格式文档的事实标准,而 poi-tl 则在 POI 基础上提供了更强大的模板引擎能力。
本文将基于官方入门教程,进行大幅度的充实和扩展,提供完整可运行的示例代码 ,并对表格合并、图片插入、文档合并等高级操作做深入演示。无论你是 POI 初学者还是有一定经验的开发者,都能从中找到实用价值。
本文所有示例基于 Apache POI 5.2.2 和 poi-tl 1.12.2。
一、环境搭建
1.1 Maven 依赖
在 pom.xml 中添加以下依赖:
xml
<!-- Apache POI 核心依赖 -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.2.2</version>
</dependency>
<!-- poi-tl:Word 模板引擎,提供增强 API -->
<dependency>
<groupId>com.deepoove</groupId>
<artifactId>poi-tl</artifactId>
<version>1.12.2</version>
</dependency>
1.2 核心类速览
| 类 | 说明 |
|---|---|
XWPFDocument |
Word 文档对象,整个 .docx 文件的入口 |
XWPFParagraph |
段落,文档的基本组成单元 |
XWPFRun |
段落内的文本片段,可独立设置样式 |
XWPFTable |
表格,支持动态创建、合并单元格、设置边框等 |
XWPFPicture |
图片对象 |
二、文档基础操作
2.1 创建与读取文档
java
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import java.io.FileInputStream;
import java.io.FileOutputStream;
// 创建新文档
XWPFDocument doc = new XWPFDocument();
// 读取已有文档
XWPFDocument existingDoc = new XWPFDocument(
new FileInputStream("./template.docx")
);
// 读取文档内容
List<XWPFParagraph> paragraphs = existingDoc.getParagraphs();
List<XWPFTable> tables = existingDoc.getTables();
List<XWPFPictureData> pictures = existingDoc.getAllPictures();
// 写入文档
try (FileOutputStream out = new FileOutputStream("output.docx")) {
doc.write(out);
}
2.2 段落与文本样式
java
// 创建段落并设置对齐
XWPFParagraph para = doc.createParagraph();
para.setAlignment(ParagraphAlignment.CENTER);
para.setIndentationFirstLine(600); // 首行缩进,单位 TWIP
// 创建 Run 并设置样式
XWPFRun run = para.createRun();
run.setText("Hello World!");
run.setBold(true);
run.setFontSize(14);
run.setFontFamily("宋体"); // 解决中文乱码的关键
run.setColor("FF0000"); // 红色
run.addBreak(); // 换行
// 插入文本到指定位置
XWPFRun newRun = para.insertNewRun(0);
newRun.setText("在段落开头插入");
三、表格操作深入
3.1 创建表格与填充数据
java
// 创建 3 行 3 列的表格
XWPFTable table = doc.createTable(3, 3);
// 设置列宽
CTTblGrid grid = table.getCTTbl().addNewTblGrid();
grid.addNewGridCol().setW(3000);
grid.addNewGridCol().setW(3000);
grid.addNewGridCol().setW(3000);
// 填充表头
XWPFTableRow headerRow = table.getRow(0);
headerRow.getCell(0).setText("姓名");
headerRow.getCell(1).setText("部门");
headerRow.getCell(2).setText("职位");
// 填充数据行
table.getRow(1).getCell(0).setText("张三");
table.getRow(1).getCell(1).setText("技术部");
table.getRow(1).getCell(2).setText("高级工程师");
3.2 表格样式设置
java
// 设置单元格背景色
XWPFTableCell cell = table.getRow(0).getCell(0);
cell.setColor("D3D3D3"); // 浅灰色
// 设置单元格内段落对齐
XWPFParagraph cellPara = cell.getParagraphs().get(0);
cellPara.setAlignment(ParagraphAlignment.CENTER);
// 设置表格整体居中(使用 poi-tl 的 TableStyle)
TableStyle style = new TableStyle();
style.setAlign(STJc.CENTER);
TableTools.styleTable(table, style);
3.3 表格合并 ------ 核心深入
表格合并是 Word 操作中的高频需求,也是相对复杂的部分。poi-tl 的 TableTools 提供了极简的 API。
3.3.1 水平合并(跨列合并)
合并同一行的多个连续单元格:
java
import com.deepoove.poi.util.TableTools;
// 合并第 1 行(索引 0)的第 0 列到第 2 列
TableTools.mergeCellsHorizonal(table, 0, 0, 2);
完整示例:创建一个带合并表头的表格
java
XWPFTable table = doc.createTable(3, 4);
// 第一行作为合并表头:合并 4 列
table.getRow(0).getCell(0).setText("员工信息汇总表");
TableTools.mergeCellsHorizonal(table, 0, 0, 3);
// 第二行作为列标题
table.getRow(1).getCell(0).setText("姓名");
table.getRow(1).getCell(1).setText("部门");
table.getRow(1).getCell(2).setText("职位");
table.getRow(1).getCell(3).setText("入职日期");
// 填充数据...
⚠️ 注意事项 :水平合并会影响后续格子的索引计算。例如,一行有 7 个格子,合并了 3 个之后,该行剩余的有效格子只有 4 个,后续操作如果索引超出 4 会报错。建议先完成所有合并操作,再填充数据。
3.3.2 垂直合并(跨行合并)
合并同一列的多个连续行:
java
// 合并第 0 列的第 0 行到第 2 行的单元格
TableTools.mergeCellsVertically(table, 0, 0, 2);
完整示例:按部门分组合并
java
XWPFTable table = doc.createTable(5, 3);
// 表头
table.getRow(0).getCell(0).setText("部门");
table.getRow(0).getCell(1).setText("姓名");
table.getRow(0).getCell(2).setText("职位");
// 数据:技术部有 3 人
table.getRow(1).getCell(0).setText("技术部");
table.getRow(1).getCell(1).setText("张三");
table.getRow(1).getCell(2).setText("架构师");
table.getRow(2).getCell(0).setText("技术部");
table.getRow(2).getCell(1).setText("李四");
table.getRow(2).getCell(2).setText("高级工程师");
table.getRow(3).getCell(0).setText("技术部");
table.getRow(3).getCell(1).setText("王五");
table.getRow(3).getCell(2).setText("工程师");
// 合并技术部所在的 3 行(第 1 行到第 3 行,第 0 列)
TableTools.mergeCellsVertically(table, 0, 1, 3);
3.3.3 原生 POI 合并方式(底层原理)
如果你不想引入 poi-tl,也可以使用原生 POI API 实现合并:
java
// 水平合并:起始单元格标记 RESTART,后续标记 CONTINUE
CTTc cell1 = table.getRow(0).getCell(0).getCTTc();
cell1.addNewTcPr().addNewHMerge().setVal(STMerge.RESTART);
CTTc cell2 = table.getRow(0).getCell(1).getCTTc();
cell2.addNewTcPr().addNewHMerge().setVal(STMerge.CONTINUE);
这种方式代码较为繁琐,推荐优先使用 TableTools。
四、图片操作深入
4.1 在段落中插入图片
java
import org.apache.poi.util.Units;
import java.io.FileInputStream;
// 创建段落
XWPFParagraph para = doc.createParagraph();
// 创建 Run
XWPFRun run = para.createRun();
// 插入图片(需指定宽度和高度,单位 EMU)
try (InputStream stream = new FileInputStream("./logo.png")) {
run.addPicture(
stream,
XWPFDocument.PICTURE_TYPE_PNG,
"logo",
Units.toEMU(200), // 宽度 200 像素
Units.toEMU(200) // 高度 200 像素
);
}
4.2 图片自适应与缩放
实际项目中,图片尺寸往往需要根据文档布局自适应。以下是一个工具方法:
java
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
public void addImageWithAutoFit(XWPFParagraph para, String imagePath,
int maxWidth, int maxHeight) throws Exception {
// 读取图片获取原始尺寸
BufferedImage image = ImageIO.read(new File(imagePath));
int origWidth = image.getWidth();
int origHeight = image.getHeight();
// 计算缩放比例
double scale = Math.min(
(double) maxWidth / origWidth,
(double) maxHeight / origHeight
);
scale = Math.min(scale, 1.0); // 不放大
int targetWidth = (int) (origWidth * scale);
int targetHeight = (int) (origHeight * scale);
XWPFRun run = para.createRun();
try (InputStream stream = new FileInputStream(imagePath)) {
run.addPicture(
stream,
XWPFDocument.PICTURE_TYPE_PNG,
"image",
Units.toEMU(targetWidth),
Units.toEMU(targetHeight)
);
}
}
4.3 在表格中插入图片
表格中的图片操作与段落类似,只需先获取单元格中的段落:
java
XWPFTableCell cell = table.getRow(0).getCell(0);
XWPFParagraph cellPara = cell.addParagraph();
XWPFRun run = cellPara.createRun();
try (InputStream stream = new FileInputStream("./avatar.png")) {
run.addPicture(
stream,
XWPFDocument.PICTURE_TYPE_PNG,
"avatar",
Units.toEMU(80),
Units.toEMU(80)
);
}
4.4 提取文档中的图片
java
XWPFDocument doc = new XWPFDocument(new FileInputStream("./doc-with-images.docx"));
List<XWPFPictureData> pictures = doc.getAllPictures();
for (int i = 0; i < pictures.size(); i++) {
XWPFPictureData picture = pictures.get(i);
byte[] data = picture.getData();
// 保存到本地
try (FileOutputStream out = new FileOutputStream("image_" + i + ".png")) {
out.write(data);
}
}
五、文档合并深入
5.1 使用 poi-tl 的 NiceXWPFDocument 合并
NiceXWPFDocument 是 poi-tl 对原生 XWPFDocument 的增强,提供了简洁的文档合并能力。
java
import com.deepoove.poi.NiceXWPFDocument;
// 加载主文档和子文档
NiceXWPFDocument main = new NiceXWPFDocument(
new FileInputStream("main.docx")
);
NiceXWPFDocument sub = new NiceXWPFDocument(
new FileInputStream("sub.docx")
);
// 合并(图片、表格、样式都不会丢失)
NiceXWPFDocument merged = main.merge(sub);
// 输出合并后的文档
try (FileOutputStream out = new FileOutputStream("merged.docx")) {
merged.write(out);
}
// 关闭资源
main.close();
sub.close();
merged.close();
5.2 合并多个文档
java
public NiceXWPFDocument mergeMultiple(List<NiceXWPFDocument> docList) {
if (docList == null || docList.isEmpty()) {
return null;
}
NiceXWPFDocument result = docList.get(0);
for (int i = 1; i < docList.size(); i++) {
result = result.merge(docList.get(i));
}
return result;
}
或者使用链式调用:
java
NiceXWPFDocument merged = main.merge(sub1).merge(sub2).merge(sub3);
5.3 合并时插入分隔符
有时我们需要在合并的文档之间添加分页符或分隔文本:
java
// 在合并前,先在主文档末尾添加分页符
XWPFParagraph breakPara = main.createParagraph();
XWPFRun breakRun = breakPara.createRun();
breakRun.addBreak(BreakType.PAGE);
// 然后合并
NiceXWPFDocument merged = main.merge(sub);
5.4 完整实战:多文档合并工具类
java
import com.deepoove.poi.NiceXWPFDocument;
import java.io.*;
import java.util.List;
public class WordMerger {
/**
* 合并多个 Word 文档
* @param inputPaths 输入文档路径列表
* @param outputPath 输出文档路径
* @param addPageBreak 是否在文档间添加分页
*/
public static void mergeDocuments(List<String> inputPaths,
String outputPath,
boolean addPageBreak) throws IOException {
if (inputPaths == null || inputPaths.isEmpty()) {
throw new IllegalArgumentException("输入文档列表不能为空");
}
NiceXWPFDocument merged = null;
List<NiceXWPFDocument> docs = new ArrayList<>();
try {
// 加载所有文档
for (String path : inputPaths) {
NiceXWPFDocument doc = new NiceXWPFDocument(
new FileInputStream(path)
);
docs.add(doc);
}
// 合并
merged = docs.get(0);
for (int i = 1; i < docs.size(); i++) {
if (addPageBreak) {
// 在合并前添加分页
XWPFParagraph para = merged.createParagraph();
para.createRun().addBreak(BreakType.PAGE);
}
merged = merged.merge(docs.get(i));
}
// 输出
try (FileOutputStream out = new FileOutputStream(outputPath)) {
merged.write(out);
}
} finally {
// 关闭所有文档
if (merged != null) merged.close();
for (NiceXWPFDocument doc : docs) {
if (doc != null) doc.close();
}
}
}
}
六、综合实战:生成员工信息报告
以下是一个综合示例,涵盖本文介绍的所有核心操作:
java
import org.apache.poi.xwpf.usermodel.*;
import org.apache.poi.util.Units;
import com.deepoove.poi.NiceXWPFDocument;
import com.deepoove.poi.util.TableTools;
import java.io.*;
public class EmployeeReportGenerator {
public static void main(String[] args) throws Exception {
// 1. 创建文档
XWPFDocument doc = new XWPFDocument();
// 2. 添加标题
XWPFParagraph titlePara = doc.createParagraph();
titlePara.setAlignment(ParagraphAlignment.CENTER);
XWPFRun titleRun = titlePara.createRun();
titleRun.setText("员工信息报告");
titleRun.setBold(true);
titleRun.setFontSize(24);
titleRun.addBreak();
// 3. 添加公司 Logo(图片)
XWPFParagraph logoPara = doc.createParagraph();
logoPara.setAlignment(ParagraphAlignment.CENTER);
XWPFRun logoRun = logoPara.createRun();
try (InputStream logoStream = new FileInputStream("./logo.png")) {
logoRun.addPicture(logoStream, XWPFDocument.PICTURE_TYPE_PNG,
"logo", Units.toEMU(120), Units.toEMU(120));
}
// 4. 创建员工信息表格(含合并)
XWPFTable table = doc.createTable(6, 4);
// 设置列宽
CTTblGrid grid = table.getCTTbl().addNewTblGrid();
for (int i = 0; i < 4; i++) {
grid.addNewGridCol().setW(2500);
}
// 表头行:合并 4 列作为总标题
XWPFTableRow headerRow = table.getRow(0);
headerRow.getCell(0).setText("员工信息表");
TableTools.mergeCellsHorizonal(table, 0, 0, 3);
headerRow.getCell(0).setColor("4472C4"); // 蓝色背景
// 列标题
table.getRow(1).getCell(0).setText("部门");
table.getRow(1).getCell(1).setText("姓名");
table.getRow(1).getCell(2).setText("职位");
table.getRow(1).getCell(3).setText("头像");
// 数据:技术部
table.getRow(2).getCell(0).setText("技术部");
table.getRow(2).getCell(1).setText("张三");
table.getRow(2).getCell(2).setText("架构师");
// 头像列插入图片
XWPFParagraph imgPara = table.getRow(2).getCell(3).addParagraph();
XWPFRun imgRun = imgPara.createRun();
try (InputStream avatar = new FileInputStream("./zhangsan.png")) {
imgRun.addPicture(avatar, XWPFDocument.PICTURE_TYPE_PNG,
"avatar", Units.toEMU(60), Units.toEMU(60));
}
table.getRow(3).getCell(0).setText("技术部");
table.getRow(3).getCell(1).setText("李四");
table.getRow(3).getCell(2).setText("高级工程师");
table.getRow(4).getCell(0).setText("技术部");
table.getRow(4).getCell(1).setText("王五");
table.getRow(4).getCell(2).setText("工程师");
// 合并技术部的部门列(第 2-4 行,第 0 列)
TableTools.mergeCellsVertically(table, 0, 2, 4);
// 5. 添加页脚
XWPFFooter footer = doc.createFooter(HeaderFooterType.DEFAULT);
XWPFParagraph footerPara = footer.createParagraph();
footerPara.setAlignment(ParagraphAlignment.CENTER);
XWPFRun footerRun = footerPara.createRun();
footerRun.setText("Generated by Apache POI · " + new java.util.Date());
footerRun.setFontSize(10);
footerRun.setColor("808080");
// 6. 输出文档
try (FileOutputStream out = new FileOutputStream("EmployeeReport.docx")) {
doc.write(out);
}
doc.close();
System.out.println("报告生成完成!");
}
}
七、常见问题与注意事项
7.1 中文乱码
设置字体为宋体或中文字体即可解决:
java
run.setFontFamily("宋体");
7.2 图片不显示
确保图片宽度和高度使用 Units.toEMU() 转换,且图片流在使用后正确关闭。
7.3 合并单元格顺序
建议先创建完整的表格并填充数据,再执行合并操作,避免索引错乱。
7.4 资源释放
所有 XWPFDocument 和 NiceXWPFDocument 使用后务必关闭:
java
try (XWPFDocument doc = new XWPFDocument()) {
// 操作...
}
7.5 poi-tl 仅支持 .docx
poi-tl 和 XWPF 模块仅支持 .docx 格式(Office 2007+),不支持旧的 .doc 格式。
八、总结
本文从环境搭建开始,系统介绍了 Apache POI XWPF 的核心操作,并深入演示了三个高频需求:
| 操作 | 核心 API | 难度 |
|---|---|---|
| 表格合并 | TableTools.mergeCellsHorizonal() / mergeCellsVertically() |
⭐⭐ |
| 图片插入 | XWPFRun.addPicture() |
⭐⭐ |
| 文档合并 | NiceXWPFDocument.merge() |
⭐⭐⭐ |
通过 poi-tl 提供的增强 API,原本需要数十行原生 POI 代码才能完成的表格合并和文档合并,现在只需一行代码即可实现。希望本文能帮助你在实际项目中更高效地生成 Word 文档。
参考资源: