Apache POI Word(docx) 实战教程:从入门到精通表格合并、图片插入与文档合并

前言

在日常开发中,我们经常需要动态生成 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 资源释放

所有 XWPFDocumentNiceXWPFDocument 使用后务必关闭:

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 文档。


参考资源

相关推荐
小牛不牛的程序员1 小时前
利用Claude Code构建自动化需求挖掘工具:从原理到实践
java·算法·自动化
wear工程师1 小时前
腾讯天美一面:从 HashMap 到 Spring 事务,面试官这套连环问到底在考什么
java·面试
鱼毓屿御2 小时前
Python 装饰器与函数调用机制(复习笔记 · 2026-07-07)
开发语言·python
浩瀚地学2 小时前
【Java基础复习】IO流(二)
java·开发语言·经验分享·笔记·学习
hdsoft_huge2 小时前
SpringBoot系列06:RESTful接口开发,请求参数接收、全局跨域CORS完整配置
java·spring boot
IVVi0jToe2 小时前
高效C++线程池设计与实现
java·c++·安全
^yi2 小时前
【C++】内存管理
开发语言·c++
AC赳赳老秦2 小时前
采购专员自动化:OpenClaw 自动比价、生成询价单、跟踪供应商报价进度实战指南
开发语言·数据库·人工智能·python·自动化·github·openclaw
QiLinkOS2 小时前
QiLink OS的失败数据共享平台的运作模式是否适用于所有行业?
android·开发语言·人工智能·算法·重构·开源