核心概念
Apache POI 本身并不直接支持 .docx (Office Open XML 格式) 的复杂操作,对于 Word 文档,POI 使用了一个名为 XWPF (XML Word Processing Format) 的子项目。

XWPF: 用于处理.docx格式的 Word 文档,这是目前最主流的方式。HWPF: 用于处理旧的.doc格式的 Word 文档,功能相对有限,且已不再是开发重点。
我们今天主要讨论的就是如何使用 XWPF。
第一步:添加 Maven 依赖
你需要在你的 pom.xml 文件中添加 Apache POI 的相关依赖,为了确保兼容性和功能完整性,建议添加以下核心依赖:
<dependencies>
<!-- POI Core -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>5.2.3</version> <!-- 建议使用较新版本 -->
</dependency>
<!-- POI OOXML for .docx support -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.2.3</version>
</dependency>
<!-- 如果需要处理图片,需要这个依赖 -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>ooxml-lite</artifactId>
<version>1.4</version>
</dependency>
</dependencies>
第二步:基础 Word 导出操作 (创建一个简单文档)
让我们从一个最简单的例子开始:创建一个包含文本、段落和表格的 Word 文档。
示例代码:SimpleWordExporter.java
import org.apache.poi.xwpf.usermodel.*;
import java.io.FileOutputStream;
import java.io.IOException;
public class SimpleWordExporter {
public static void main(String[] args) {
// 1. 创建一个空的 Word 文档对象
XWPFDocument document = new XWPFDocument();
try (FileOutputStream out = new FileOutputStream("D:/SimpleDocument.docx")) {
// 2. 添加一个标题
XWPFParagraph titleParagraph = document.createParagraph();
titleParagraph.setAlignment(ParagraphAlignment.CENTER); // 设置居中
XWPFRun titleRun = titleParagraph.createRun();
titleRun.setBold(true);
titleRun.setFontSize(16);
titleRun.setText("我的第一个 POI Word 文档");
// 3. 添加一个普通段落
XWPFParagraph paragraph = document.createParagraph();
XWPFRun run = paragraph.createRun();
run.setText("这是一个使用 Apache POI 创建的段落。");
run.addBreak(); // 换行
run.setText("POI 是一个非常强大的 Java 库,用于操作 Office 文件格式。");
// 4. 添加一个表格
XWPFTable table = document.createTable();
// 创建表头行
XWPFTableRow headerRow = table.getRow(0);
headerRow.getCell(0).setText("姓名");
headerRow.addNewTableCell().setText("年龄");
headerRow.addNewTableCell().setText("城市");
// 创建数据行
XWPFTableRow dataRow1 = table.createRow();
dataRow1.getCell(0).setText("张三");
dataRow1.getCell(1).setText("28");
dataRow1.getCell(2).setText("北京");
XWPFTableRow dataRow2 = table.createRow();
dataRow2.getCell(0).setText("李四");
dataRow2.getCell(1).setText("32");
dataRow2.getCell(2).setText("上海");
// 5. 将文档内容写入到输出流
document.write(out);
System.out.println("Word 文档生成成功!路径: D:/SimpleDocument.docx");
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
// 6. 关闭文档
document.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
代码解析:
XWPFDocument document = new XWPFDocument();: 创建一个代表 Word 文档的内存对象。document.createParagraph();: 创建一个段落。paragraph.createRun();: 在段落中创建一个文本运行块,一个段落可以包含多个Run,每个Run可以有独立的样式(如加粗、斜体、字体大小等)。run.setText(...): 设置Run中的文本内容。run.setBold(true): 设置文本为粗体。run.setFontSize(16): 设置字体大小。paragraph.setAlignment(ParagraphAlignment.CENTER): 设置段落的对齐方式。document.createTable();: 创建一个表格,表格由XWPFTableRow(行)和XWPFTableCell(单元格)组成。document.write(out): 将内存中的文档对象写入到指定的文件输出流。document.close(): 关闭文档,释放资源。非常重要!
第三步:复杂 Word 导出 (使用模板)
在实际项目中,我们通常需要基于一个已经设计好的 Word 模板来生成报告,这样可以保证格式、Logo、样式等的一致性。

模板文件 (template.docx)
创建一个 Word 文件,命名为 template.docx,在其中放置一些需要被动态替换的文本,例如用 ${name}, ${project}, ${date} 作为占位符。
-----------------------------
| 公司 Logo |
-----------------------------
项目报告
项目名称: ${project}
负责人: ${name}
报告日期: ${date}
${description}
-----------------------------
| (页脚) |
-----------------------------
示例代码:TemplateWordExporter.java
import org.apache.poi.xwpf.usermodel.*;
import org.apache.poi.xwpf.usermodel.XWPFRun;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class TemplateWordExporter {
public static void main(String[] args) {
String templatePath = "D:/template.docx";
String outputPath = "D:/ProjectReport.docx";
try (FileInputStream fis = new FileInputStream(templatePath);
XWPFDocument document = new XWPFDocument(fis);
FileOutputStream out = new FileOutputStream(outputPath)) {
// 准备数据
Map<String, String> data = new HashMap<>();
data.put("${project}", "智慧城市管理系统一期");
data.put("${name}", "王五");
data.put("${date}", new SimpleDateFormat("yyyy年MM月dd日").format(new Date()));
data.put("${description}", "本项目旨在构建一个集成了交通、安防、环保等多维度的智慧城市管理平台,目前一期已完成核心架构搭建和三个子模块的开发。");
// 遍历文档中的所有段落
for (XWPFParagraph p : document.getParagraphs()) {
replaceTextInParagraph(p, data);
}
// 遍历文档中的所有表格
for (XWPFTable table : document.getTables()) {
for (XWPFTableRow row : table.getRows()) {
for (XWPFTableCell cell : row.getTableCells()) {
for (XWPFParagraph p : cell.getParagraphs()) {
replaceTextInParagraph(p, data);
}
}
}
}
// 写入新文件
document.write(out);
System.out.println("基于模板的 Word 文档生成成功!路径: " + outputPath);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 在段落中替换文本
* @param paragraph 段落对象
* @param data 数据Map
*/
private static void replaceTextInParagraph(XWPFParagraph paragraph, Map<String, String> data) {
String paragraphText = paragraph.getText();
// 如果段落中不包含任何占位符,则跳过
if (!paragraphText.contains("${")) {
return;
}
// 创建一个新的段落来替换原来的段落,因为直接修改 Run 的文本比较麻烦
// 特别是当占位符跨越多个 Run 时
XWPFParagraph newParagraph = paragraph.getBody().insertNewParagraph(paragraph.getCTP());
int lastPos = 0;
int startPos;
while ((startPos = paragraphText.indexOf("${", lastPos)) != -1) {
int endPos = paragraphText.indexOf("}", startPos);
if (endPos == -1) break;
// 添加占位符之前的文本
if (startPos > lastPos) {
String textBefore = paragraphText.substring(lastPos, startPos);
XWPFRun run = newParagraph.createRun();
run.setText(textBefore);
}
// 获取占位符键并替换
String key = paragraphText.substring(startPos, endPos + 1);
String replacement = data.getOrDefault(key, key); // 如果找不到对应值,保留原占位符
XWPFRun replacementRun = newParagraph.createRun();
replacementRun.setText(replacement);
lastPos = endPos + 1;
}
// 添加最后一个占位符之后的文本
if (lastPos < paragraphText.length()) {
String textAfter = paragraphText.substring(lastPos);
XWPFRun run = newParagraph.createRun();
run.setText(textAfter);
}
// 删除原始段落
paragraph.remove();
}
}
代码解析:
FileInputStream fis = new FileInputStream(templatePath): 读取模板文件。new XWPFDocument(fis): 基于模板文件流创建文档对象。document.getParagraphs()和document.getTables(): 获取文档中所有的段落和表格。replaceTextInParagraph(...): 这是一个核心的自定义方法,用于在单个段落中查找并替换${key}格式的文本。- 注意: 直接修改
Run的文本来处理跨越多个Run的占位符非常复杂,上述代码采用了一种更稳健的策略:创建一个新的段落,将替换后的内容按顺序写入新段落,最后删除旧段落,这在大多数情况下是可行的。
- 注意: 直接修改
cell.getParagraphs(): 遍历表格单元格中的所有段落,因为文本也可能在单元格的段落里。
第四步:高级用法 (图片、列表、页眉页脚)
插入图片
// ... 在某个段落或 Run 之后
XWPFParagraph pictureParagraph = document.createParagraph();
XWPFRun pictureRun = pictureParagraph.createRun();
// 从文件系统读取图片
try (FileInputStream picInputStream = new FileInputStream("D:/logo.png")) {
// 第一个参数是图片数据流
// 第二个参数是图片格式 (如 "png", "jpg")
// 第三个参数是图片宽度 (单位:EMU, 1 inch = 9144000 EMU)
// 第四个参数是图片高度
pictureRun.addPicture(picInputStream, XWPFDocument.PICTURE_TYPE_PNG, "logo.png UnitsPerInch/* 96, 5000000, 2000000);
} catch (Exception e) {
e.printStackTrace();
}
创建列表
XWPFParagraph listParagraph = document.createParagraph();
// 设置为项目符号列表
listParagraph.setNumID(listParagraph.getCTP().addNewPPr().addNewNumPr().addNewNumId());
// 或者设置为编号列表
// listParagraph.setNumID(...);
XWPFRun run1 = listParagraph.createRun();
run1.setText("第一项列表内容");
XWPFParagraph listParagraph2 = document.createParagraph();
listParagraph2.setNumID(listParagraph.getCTP().getPPr().getNumPr().getNumId());
XWPFRun run2 = listParagraph2.createRun();
run2.setText("第二项列表内容");
注意: POI 中创建复杂列表(如多级列表)比较繁琐,通常建议在模板中预先设置好列表格式,然后只填充文本。
添加页眉页脚
// 获取文档的页眉
XWPFHeader header = document.createHeader(HeaderFooterType.DEFAULT);
// 在页眉中添加段落和文本
XWPFParagraph headerParagraph = header.createParagraph();
XWPFRun headerRun = headerParagraph.createRun();
headerRun.setText("这是页脚内容 - 第 ");
headerRun.setBold(true);
// 添加页码
headerRun = headerParagraph.createRun();
headerRun.addTab(); // 添加一个制表符
headerRun = headerParagraph.createRun();
headerRun.setText(" 页");
// 添加域代码来显示实际的页码
CTP ctp = headerParagraph.getCTP();
// 这是一个简化的页码添加方式,复杂页码可能需要直接操作 XML
// 实际项目中,更推荐在模板中设置好页码格式,然后通过 POI 来控制
总结与最佳实践
-
选择合适的方法:
- 从零开始创建: 适用于格式非常简单、动态性不高的文档。
- 基于模板: 强烈推荐,适用于绝大多数业务场景,能完美保留复杂的格式、样式和布局。
-
性能考虑: 对于非常大的文档(如几百页),POI 会占用较多内存,可以考虑使用
SXSSF(流式 API) 的思想,但 POI 本身没有为 Word 提供直接的流式 API,如果内存成为瓶颈,可能需要考虑其他方案或优化数据加载方式。
(图片来源网络,侵删) -
样式处理: POI 提供了丰富的样式 API,但直接通过代码设置所有样式(如字体、颜色、行间距、段间距等)非常繁琐,最佳实践是:
- 在 Word 模板中预先定义好样式(创建一个“标题1”样式)。
- 在代码中,通过
paragraph.setStyle("标题1")来应用模板中定义好的样式。
-
复杂性管理: 当 Word 文档变得非常复杂(包含复杂的表格、图文混排、多级列表等)时,POI 的代码会变得难以维护,保持模板的整洁和代码的逻辑清晰就至关重要。
希望这份详细的指南能帮助你顺利地在 Java 项目中使用 POI 导出 Word 文档!
