杰瑞科技汇

Java POI如何实现Word文档导出?

核心概念

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

Java POI如何实现Word文档导出?-图1
(图片来源网络,侵删)
  • 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();
            }
        }
    }
}

代码解析:

  1. XWPFDocument document = new XWPFDocument();: 创建一个代表 Word 文档的内存对象。
  2. document.createParagraph();: 创建一个段落。
  3. paragraph.createRun();: 在段落中创建一个文本运行块,一个段落可以包含多个 Run,每个 Run 可以有独立的样式(如加粗、斜体、字体大小等)。
  4. run.setText(...): 设置 Run 中的文本内容。
  5. run.setBold(true): 设置文本为粗体。
  6. run.setFontSize(16): 设置字体大小。
  7. paragraph.setAlignment(ParagraphAlignment.CENTER): 设置段落的对齐方式。
  8. document.createTable();: 创建一个表格,表格由 XWPFTableRow(行)和 XWPFTableCell(单元格)组成。
  9. document.write(out): 将内存中的文档对象写入到指定的文件输出流。
  10. document.close(): 关闭文档,释放资源。非常重要!

第三步:复杂 Word 导出 (使用模板)

在实际项目中,我们通常需要基于一个已经设计好的 Word 模板来生成报告,这样可以保证格式、Logo、样式等的一致性。

Java POI如何实现Word文档导出?-图2
(图片来源网络,侵删)

模板文件 (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();
    }
}

代码解析:

  1. FileInputStream fis = new FileInputStream(templatePath): 读取模板文件。
  2. new XWPFDocument(fis): 基于模板文件流创建文档对象。
  3. document.getParagraphs()document.getTables(): 获取文档中所有的段落和表格。
  4. replaceTextInParagraph(...): 这是一个核心的自定义方法,用于在单个段落中查找并替换 ${key} 格式的文本。
    • 注意: 直接修改 Run 的文本来处理跨越多个 Run 的占位符非常复杂,上述代码采用了一种更稳健的策略:创建一个新的段落,将替换后的内容按顺序写入新段落,最后删除旧段落,这在大多数情况下是可行的。
  5. 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 来控制

总结与最佳实践

  1. 选择合适的方法:

    • 从零开始创建: 适用于格式非常简单、动态性不高的文档。
    • 基于模板: 强烈推荐,适用于绝大多数业务场景,能完美保留复杂的格式、样式和布局。
  2. 性能考虑: 对于非常大的文档(如几百页),POI 会占用较多内存,可以考虑使用 SXSSF (流式 API) 的思想,但 POI 本身没有为 Word 提供直接的流式 API,如果内存成为瓶颈,可能需要考虑其他方案或优化数据加载方式。

    Java POI如何实现Word文档导出?-图3
    (图片来源网络,侵删)
  3. 样式处理: POI 提供了丰富的样式 API,但直接通过代码设置所有样式(如字体、颜色、行间距、段间距等)非常繁琐,最佳实践是:

    • 在 Word 模板中预先定义好样式(创建一个“标题1”样式)。
    • 在代码中,通过 paragraph.setStyle("标题1") 来应用模板中定义好的样式。
  4. 复杂性管理: 当 Word 文档变得非常复杂(包含复杂的表格、图文混排、多级列表等)时,POI 的代码会变得难以维护,保持模板的整洁和代码的逻辑清晰就至关重要。

希望这份详细的指南能帮助你顺利地在 Java 项目中使用 POI 导出 Word 文档!

分享:
扫描分享到社交APP
上一篇
下一篇