Apache POI 适合把 Java 应用中的数据按固定模板批量写入 Excel、Word 或 PowerPoint 文件,并将结果保存为可重复执行的报表流程。你可以用它处理 .xlsx、.docx 等 Office 文档,但需要编写和维护 Java 代码;如果你希望完全通过界面操作、无需开发,就不适合把 POI 当作零代码工具。
1. 先确认文件类型与处理范围
Apache POI 按 Office 文件格式提供不同 API。处理旧版 Excel .xls 通常使用 HSSF,处理 .xlsx 使用 XSSF;处理 .docx 使用 XWPF,处理 .pptx 使用 XSLF。本文示例聚焦 .xlsx 和 .docx。
如果一批数据要生成大量 Excel 文件,或单个工作簿包含很多行,普通 XSSF 可能带来较高的内存占用。POI 提供 SXSSF 用于流式生成大型工作簿,但它主要适合按顺序写出数据,不能简单视作完整 XSSF 的低内存替代品。选型前应先确认是否需要读取既有模板、回改已有单元格或访问整张表。
2. 配置 Maven 或 Gradle 依赖
处理 .xlsx 和 .docx 时,通常需要 poi-ooxml。它包含 OOXML 文档格式相关 API;如果只处理旧版 .xls,再根据项目需要使用 poi。
先在项目的依赖管理中选定一个经过团队验证的 POI 版本,并确保相关 POI 模块使用同一版本。下面用属性表示版本,避免把示例版本误当作当前推荐版本。
Maven:
<properties>
<poi.version>替换为项目采用的版本</poi.version>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>${poi.version}</version>
</dependency>
</dependencies>
Gradle:
dependencies {
implementation "org.apache.poi:poi-ooxml:替换为项目采用的版本"
}
依赖准备完成后,先在开发环境用少量模板文件做读写测试。若项目已有依赖管理规则,应遵循项目的版本约束,并检查是否存在多个 POI 版本同时进入运行时类路径的情况。
3. 读取模板并生成 Excel 报表
模板文件可预先放在应用资源目录或由调用方传入。生产环境通常应从可配置位置读取模板和输出目录,不要把个人电脑路径写死在代码中。
下面示例读取一个 .xlsx 模板,向指定工作表写入数据,再另存为新文件:
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
public class ExcelReportGenerator {
public static void generate(
Path templatePath,
Path outputPath,
List<ReportRow> rows
) throws Exception {
try (InputStream input = Files.newInputStream(templatePath);
Workbook workbook = new XSSFWorkbook(input)) {
Sheet sheet = workbook.getSheet("报表");
if (sheet == null) {
throw new IllegalArgumentException("模板中缺少“报表”工作表");
}
int rowIndex = 1; // 假设第 0 行是表头
for (ReportRow item : rows) {
Row row = sheet.getRow(rowIndex);
if (row == null) {
row = sheet.createRow(rowIndex);
}
setText(row, 0, item.name());
setNumber(row, 1, item.amount());
setText(row, 2, item.reportDate().toString());
rowIndex++;
}
try (OutputStream output = Files.newOutputStream(outputPath)) {
workbook.write(output);
}
}
}
private static void setText(Row row, int columnIndex, String value) {
Cell cell = row.getCell(columnIndex);
if (cell == null) {
cell = row.createCell(columnIndex);
}
cell.setCellValue(value == null ? "" : value);
}
private static void setNumber(Row row, int columnIndex, double value) {
Cell cell = row.getCell(columnIndex);
if (cell == null) {
cell = row.createCell(columnIndex);
}
cell.setCellValue(value);
}
}
示例中的 ReportRow 是你的业务数据类型,需替换为实际类。工作表名称、起始行和列号也要按模板调整。使用模板时,先检查目标区域是否已有样式、合并单元格或公式;直接新建行或单元格可能改变模板原有布局。
日期不要只依赖字符串显示。若后续还要排序、筛选或计算,应把日期写入日期单元格,并为单元格设置日期格式;字符串形式更适合纯展示,但在 Excel 中通常不能当作日期值参与计算。单元格样式可从模板中的对应单元格复制,或由代码统一创建。创建大量单元格时,避免为每个单元格重复创建样式对象。
公式与计算结果
POI 可以把公式写入单元格,例如:
Cell total = row.createCell(3);
total.setCellFormula("SUM(B2:C2)");
设置公式不等于已经验证 Excel 中显示的计算结果。若需要在程序中计算公式,可在写出前使用工作簿的公式计算器;也可以让 Excel 打开文件后重新计算。复杂公式、外部引用或不同 Office 软件之间的计算差异,应使用实际模板和目标软件验证。
4. 用 Word 模板替换占位内容
Word 模板常用 {{客户名称}}、{{报告日期}} 这样的标记。简单文档可遍历段落和表格中的文本片段并替换;但 Word 可能把一个占位符拆分成多个文本 run,例如 {{客户 和 名称}} 分属不同 run。此时只逐个 run 查找完整标记会失败。
因此,替换前要用真实模板测试占位符是否保持在同一文本片段。需要保留局部字体、加粗或颜色时,直接合并并重写整段文字也可能破坏原有格式。对复杂模板,应按段落结构处理,或先统一模板中的占位符格式,避免占位符跨越不同格式片段。
下面的示例演示一个基础段落替换方法,适用于占位符完整位于同一 run 的情况:
import org.apache.poi.xwpf.usermodel.*;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
public class WordReportGenerator {
public static void generate(
Path templatePath,
Path outputPath,
Map<String, String> values
) throws Exception {
try (InputStream input = Files.newInputStream(templatePath);
XWPFDocument document = new XWPFDocument(input)) {
for (XWPFParagraph paragraph : document.getParagraphs()) {
replaceRuns(paragraph, values);
}
for (XWPFTable table : document.getTables()) {
for (XWPFTableRow row : table.getRows()) {
for (XWPFTableCell cell : row.getTableCells()) {
for (XWPFParagraph paragraph : cell.getParagraphs()) {
replaceRuns(paragraph, values);
}
}
}
}
try (OutputStream output = Files.newOutputStream(outputPath)) {
document.write(output);
}
}
}
private static void replaceRuns(
XWPFParagraph paragraph,
Map<String, String> values
) {
for (XWPFRun run : paragraph.getRuns()) {
String text = run.getText(0);
if (text == null) {
continue;
}
for (Map.Entry<String, String> entry : values.entrySet()) {
text = text.replace(
"{{" + entry.getKey() + "}}",
entry.getValue() == null ? "" : entry.getValue()
);
}
run.setText(text, 0);
}
}
}
这个简化实现遍历了正文段落和表格单元格,但没有覆盖页眉、页脚、文本框等所有文档区域。若模板使用这些结构,应单独实现遍历和替换,并通过输出文件检查结果。对表格行数会随数据变化的报表,还需要按模板规则复制行、单元格及格式,不能只替换固定位置的占位符。
5. 批量运行与结果校验
批处理的重点不只是成功写出文件,还要能发现缺失、损坏或内容不完整的报表。建议按以下顺序组织任务:
- 校验输入。 检查模板是否存在、数据是否完整、输出目录是否可写,并提前处理重复文件名。
- 逐条生成。 每次处理一个报表或一小批数据,使用独立输出文件名;不要让多个任务同时写入同一个目标文件。
- 记录结果。 记录输入标识、输出路径、处理时间和异常信息。批次中单个文件失败时,可按业务要求继续处理其他文件,并汇总失败项。
- 检查产物。 检查文件是否存在且大小合理,再用 POI 尝试重新打开输出文件。关键报表还应抽查工作表、行数、占位符残留、日期格式和公式单元格。
- 对照业务数据。 比较生成文件中的记录数、合计值或关键字段,避免只凭“文件成功写出”判断报表正确。
不要把敏感数据或完整文档内容直接写入日志。记录足够定位问题的信息即可,例如任务编号、文件名、异常类型和堆栈摘要。
6. 常见问题与处理方式
内存持续增长或批处理失败
大型工作簿和大量并发任务会增加内存压力。先限制并发数、避免同时保留多个完整文档对象,并及时关闭输入流、工作簿和输出流。生成超大 .xlsx 文件时评估 SXSSF,但要确认模板读写方式与所需操作是否受支持,并按所用 POI 版本的 API 要求清理临时文件。
输出文件被占用或覆盖失败
文件被 Excel、Word 或其他进程打开时,写入可能失败,具体表现取决于操作系统和写入方式。批处理尽量写入独立的新文件,完成后再按明确规则移动或归档;遇到文件锁定时记录目标路径并重试有限次数,不要无限重试。
日期、公式或格式与预期不符
先区分单元格实际保存的是日期值还是文本,再检查日期格式;公式则检查公式表达式、引用范围以及是否需要重新计算。格式应以真实模板为准进行验证,不要只检查代码没有抛出异常。
Word 占位符没有替换
检查标记是否拼写一致、是否被拆分到多个 run,以及它是否位于正文、表格、页眉或其他区域。先用最小模板定位问题,再决定是调整模板写法,还是扩展遍历逻辑。
7. 适用性判断
如果你的报表流程已有 Java 数据源,模板结构相对稳定,而且需要周期性、批量、可重复地生成 Office 文件,Apache POI 是值得评估的方案。它覆盖 Excel、Word 和 PowerPoint 等文档格式,并提供程序化读写能力;相应地,模板变化、格式兼容、公式计算和错误恢复都需要开发与测试。
如果任务只是偶尔编辑一两个文件,或者你不打算维护代码,直接使用 Office 软件或其他可视化自动化工具通常更合适。 POI 解决的是“把文档生成流程纳入 Java 程序”,不是免配置、免编码的报表设计器。

评论列表 (1条):
加载更多评论 Loading...