程序运行时,POI 会读取这个模板文件,找到占位符,用实际数据替换它们,最后生成一个新的 Excel 文件,同时完美保留了模板的所有格式。

下面我将为你提供一个从入门到精通的详细指南,包含核心概念、操作步骤、代码示例和最佳实践。
目录
- 核心概念:为什么使用模板?
- 准备工作:添加 POI 依赖
- 模板文件设计:如何创建和使用占位符
- 核心代码实现:读取、填充、写入
- 示例1:简单数据替换
- 示例2:列表数据填充(最常用)
- 示例3:高级操作(图片、公式、富文本)
- 完整代码示例
- 最佳实践与注意事项
核心概念:为什么使用模板?
直接用 POI 从零开始创建 Excel 文件(如 XSSFWorkbook)非常繁琐,你需要手动设置每一个单元格的样式(CellStyle)、字体(Font)、行高(RowHeight)等,对于复杂的报表,这几乎是不可能的任务。
使用模板的优势:
- 格式与数据分离:美工或业务人员可以用 Excel 自身强大的功能设计报表样式,程序员只需关注数据填充。
- 高效开发:避免编写大量样式设置代码,极大提高开发效率。
- 易于维护:当报表样式需要调整时,只需修改
.xlsx模板文件,无需改动 Java 代码。 - 功能强大:可以轻松实现复杂布局,如合并单元格、图表、图片嵌入等。
准备工作:添加 POI 依赖
如果你使用 Maven,在 pom.xml 中添加以下依赖,注意,POI 5.x 版本对 API 进行了优化,推荐使用。

<dependencies>
<!-- POI Core -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>5.2.5</version>
</dependency>
<!-- For .xlsx format (Office 2007 and later) -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.2.5</version>
</dependency>
<!-- For using the XSSF (OOXML) and SXSSF (Big Data) API -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml-lite</artifactId>
<version>5.2.5</version>
</dependency>
</dependencies>
模板文件设计:如何创建和使用占位符
- 创建一个 Excel 文件 (
template.xlsx),用 Excel 软件设计好你想要的布局、样式、标题等。 - 定义占位符:
- 简单变量:在需要填充单个数据(如用户名、日期、总计)的单元格中,输入类似
${username},${date},${total}这样的文本。 - 列表数据:对于需要重复填充的多行数据,设计一个“模板行”,并在该行的某些列中使用占位符,如
${item.name},${item.price},通常我们会用一行数据作为模板,POI 会根据数据量复制这一行。
- 简单变量:在需要填充单个数据(如用户名、日期、总计)的单元格中,输入类似
示例模板 (template.xlsx)
| A | B | C | D | |
|---|---|---|---|---|
| 1 | 销售报表 | |||
| 2 | ||||
| 3 | 客户姓名 | 购买商品 | 数量 | 单价 |
| 4 | ${customer.name} |
${customer.product} |
${customer.quantity} |
${customer.price} |
| 5 | ||||
| 6 | 生成日期 | ${report.date} |
总计 | ${total.amount} |
| 7 |
核心代码实现:读取、填充、写入
POI 提供了 XSSF 和 SXSSF 两种 API 来处理 .xlsx 文件。XSSF 将整个文件加载到内存中,适合中小文件;SXSSF 采用流式处理,适合大数据量,但会限制对已写入行的修改,对于模板操作,通常使用 XSSF。
示例1:简单数据替换
这个例子将替换 ${customer.name}, ${report.date} 等简单变量。
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
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 SimpleTemplateExample {
public static void main(String[] args) {
// 1. 定义模板路径和输出路径
String templatePath = "template.xlsx";
String outputPath = "output_simple.xlsx";
// 2. 准备要填充的数据 (Map结构,键是模板中的占位符)
Map<String, String> data = new HashMap<>();
data.put("${customer.name}", "张三");
data.put("${customer.product}", "笔记本电脑");
data.put("${customer.quantity}", "1");
data.put("${customer.price}", "5999.00");
data.put("${report.date}", new SimpleDateFormat("yyyy-MM-dd").format(new Date()));
data.put("${total.amount}", "5999.00");
try (FileInputStream fis = new FileInputStream(templatePath);
Workbook workbook = new XSSFWorkbook(fis);
FileOutputStream fos = new FileOutputStream(outputPath)) {
// 3. 获取第一个工作表
Sheet sheet = workbook.getSheetAt(0);
// 4. 遍历所有行
for (Row row : sheet) {
// 5. 遍历行中的所有单元格
for (Cell cell : row) {
// 6. 检查单元格类型是否为字符串
if (cell.getCellType() == CellType.STRING) {
String cellValue = cell.getStringCellValue();
// 7. 检查字符串是否包含我们的占位符
if (data.containsKey(cellValue)) {
// 8. 替换占位符为实际数据
cell.setCellValue(data.get(cellValue));
}
}
}
}
// 9. 将修改后的工作簿写入输出文件
workbook.write(fos);
System.out.println("简单模板填充完成,文件已生成: " + outputPath);
} catch (IOException e) {
e.printStackTrace();
}
}
}
示例2:列表数据填充(最常用)
这是最核心的功能,用于填充动态生成的列表,POI 5.x 引入了 SheetUtil 和 RowUtil,使得操作更直观,但更传统和通用的方法是找到模板行,然后循环复制并填充。

import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class ListTemplateExample {
public static void main(String[] args) {
String templatePath = "template.xlsx";
String outputPath = "output_list.xlsx";
// 1. 准备列表数据
List<Map<String, String>> items = new ArrayList<>();
items.add(createItem("李四", "无线鼠标", "2", "89.90"));
items.add(createItem("王五", "机械键盘", "1", "459.00"));
items.add(createItem("赵六", "USB Hub", "1", "128.00"));
// 2. 准备单行数据
Map<String, String> summaryData = new HashMap<>();
summaryData.put("${customer.name}", "客户总览");
summaryData.put("${report.date}", new SimpleDateFormat("yyyy-MM-dd").format(new Date()));
// 计算总价
double total = items.stream().mapToDouble(item -> Double.parseDouble(item.get("${item.price}")) * Integer.parseInt(item.get("${item.quantity}"))).sum();
summaryData.put("${total.amount}", String.format("%.2f", total));
try (FileInputStream fis = new FileInputStream(templatePath);
Workbook workbook = new XSSFWorkbook(fis);
FileOutputStream fos = new FileOutputStream(outputPath)) {
Sheet sheet = workbook.getSheetAt(0);
// 找到列表数据的模板行(这里是第4行,索引为3)
Row templateRow = sheet.getRow(3);
if (templateRow == null) {
System.out.println("未找到模板行,请检查模板文件。");
return;
}
// 从第5行(索引为4)开始插入数据
int insertRowIndex = 4;
// 3. 遍历列表数据
for (Map<String, String> itemData : items) {
// 4. 复制模板行
Row newRow = sheet.createRow(insertRowIndex);
copyRow(workbook, templateRow, newRow);
// 5. 在新行中填充数据
for (Cell cell : newRow) {
if (cell.getCellType() == CellType.STRING) {
String cellValue = cell.getStringCellValue();
// 注意:这里的占位符是 ${item.xxx},因为我们知道是列表项
if (cellValue.startsWith("${item.")) {
String key = cellValue.replace("${item.", "").replace("}", "");
cell.setCellValue(itemData.get("${" + key + "}"));
}
}
}
insertRowIndex++;
}
// 6. 替换摘要数据(和示例1一样)
for (Row row : sheet) {
for (Cell cell : row) {
if (cell.getCellType() == CellType.STRING && summaryData.containsKey(cell.getStringCellValue())) {
cell.setCellValue(summaryData.get(cell.getStringCellValue()));
}
}
}
workbook.write(fos);
System.out.println("列表模板填充完成,文件已生成: " + outputPath);
} catch (IOException e) {
e.printStackTrace();
}
}
// 辅助方法:创建一个项目数据Map
private static Map<String, String> createItem(String name, String product, String quantity, String price) {
Map<String, String> item = new HashMap<>();
item.put("${item.name}", name);
item.put("${item.product}", product);
item.put("${item.quantity}", quantity);
item.put("${item.price}", price);
return item;
}
// 辅助方法:复制一行及其样式
private static void copyRow(Workbook workbook, Row sourceRow, Row targetRow) {
// 复制行高
targetRow.setHeight(sourceRow.getHeight());
// 复制单元格
for (Cell sourceCell : sourceRow) {
Cell targetCell = targetRow.createCell(sourceCell.getColumnIndex());
// 复制单元格类型和值
targetCell.setCellType(sourceCell.getCellType());
switch (sourceCell.getCellType()) {
case STRING:
targetCell.setCellValue(sourceCell.getStringCellValue());
break;
case NUMERIC:
targetCell.setCellValue(sourceCell.getNumericCellValue());
break;
case BOOLEAN:
targetCell.setCellValue(sourceCell.getBooleanCellValue());
break;
case FORMULA:
targetCell.setCellFormula(sourceCell.getCellFormula());
break;
// ... 其他类型
}
// 复制单元格样式
CellStyle sourceCellStyle = sourceCell.getCellStyle();
if (sourceCellStyle != null) {
CellStyle targetCellStyle = workbook.createCellStyle();
targetCellStyle.cloneStyleFrom(sourceCellStyle);
targetCell.setCellStyle(targetCellStyle);
}
}
}
}
示例3:高级操作(图片、公式、富文本)
-
插入图片:
// 在某个位置插入图片 int pictureIdx = workbook.addPicture(new FileInputStream("logo.png"), Workbook.PICTURE_TYPE_PNG); Drawing<?> drawing = sheet.createDrawingPatriarch(); ClientAnchor anchor = drawing.createAnchor(0, 0, 0, 0, 0, 0, 1, 1); // (col1, row1, col2, row2) Picture pict = drawing.createPicture(anchor, pictureIdx); pict.resize(); -
使用公式:
// 在模板中直接写公式,如 ${total.formula} // 在代码中找到这个单元格,把占位符替换成公式字符串 cell.setCellValue("SUM(D4:D6)"); -
富文本(Rich Text):
RichTextString rts = workbook.getCreationHelper().createRichTextString("红色文字"); Font redFont = workbook.createFont(); redFont.setColor(IndexedColors.RED.getIndex()); rts.applyFont(redFont); cell.setCellValue(rts);
完整代码示例
结合以上所有示例,这里是一个更完整的 ListTemplateExample,它包含了列表填充和简单替换。
// ... (imports and main method from ListTemplateExample) ...
public class ListTemplateExample {
public static void main(String[] args) {
// ... (data preparation from ListTemplateExample) ...
try (FileInputStream fis = new FileInputStream(templatePath);
Workbook workbook = new XSSFWorkbook(fis);
FileOutputStream fos = new FileOutputStream(outputPath)) {
Sheet sheet = workbook.getSheetAt(0);
Row templateRow = sheet.getRow(3); // 模板行
int insertRowIndex = 4; // 插入起始行
// --- 1. 填充列表数据 ---
for (Map<String, String> itemData : items) {
Row newRow = sheet.createRow(insertRowIndex);
copyRow(workbook, templateRow, newRow);
for (Cell cell : newRow) {
if (cell.getCellType() == CellType.STRING) {
String cellValue = cell.getStringCellValue();
if (cellValue.startsWith("${item.")) {
String key = cellValue.replace("${item.", "").replace("}", "");
cell.setCellValue(itemData.get("${" + key + "}"));
}
}
}
insertRowIndex++;
}
// --- 2. 替换摘要数据 ---
for (Row row : sheet) {
for (Cell cell : row) {
if (cell.getCellType() == CellType.STRING) {
String cellValue = cell.getStringCellValue();
// 处理摘要数据
if (summaryData.containsKey(cellValue)) {
cell.setCellValue(summaryData.get(cellValue));
}
// 处理公式
if ("${total.formula}".equals(cellValue)) {
cell.setCellValue("SUM(D4:D" + (insertRowIndex - 1) + ")");
}
}
}
}
workbook.write(fos);
System.out.println("完整模板填充完成,文件已生成: " + outputPath);
} catch (IOException e) {
e.printStackTrace();
}
}
// ... (createItem and copyRow helper methods) ...
}
最佳实践与注意事项
- 模板文件是关键:花时间设计一个清晰、易于维护的模板,使用明确的占位符命名(如
${report.startDate}而不是${date1})。 - 处理大数据量:如果列表数据量非常大(例如超过 1 万行),使用
SXSSFWorkbook会更节省内存,但请注意,SXSSFWorkbook在写入后就不能再修改之前的行,所以你需要一次性计算好所有数据再写入。 - 日期和数字格式:模板中预先设置好单元格的格式(如
yyyy-MM-dd或#,##0.00),POI 在替换字符串时不会改变单元格的格式,但如果替换的是数字类型,则依赖你设置的数字格式。 - 性能优化:避免在循环中重复创建
CellStyle或Font,应该在循环外部创建好,然后在循环中引用它们。 - 异常处理:文件操作(IO)和 POI 操作都可能抛出异常,务必使用
try-with-resources或try-catch-finally来确保资源(如FileInputStream,Workbook)被正确关闭。 - 测试:用各种边界情况测试你的模板,例如空数据列表、超长文本、特殊字符等,确保生成的 Excel 文件符合预期。
希望这份详细的指南能帮助你熟练掌握 Java POI 操作 Excel 模板!
