杰瑞科技汇

Java POI如何高效处理Excel模板?

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

Java POI如何高效处理Excel模板?-图1
(图片来源网络,侵删)

下面我将为你提供一个从入门到精通的详细指南,包含核心概念、操作步骤、代码示例和最佳实践。


目录

  1. 核心概念:为什么使用模板?
  2. 准备工作:添加 POI 依赖
  3. 模板文件设计:如何创建和使用占位符
  4. 核心代码实现:读取、填充、写入
    • 示例1:简单数据替换
    • 示例2:列表数据填充(最常用)
    • 示例3:高级操作(图片、公式、富文本)
  5. 完整代码示例
  6. 最佳实践与注意事项

核心概念:为什么使用模板?

直接用 POI 从零开始创建 Excel 文件(如 XSSFWorkbook)非常繁琐,你需要手动设置每一个单元格的样式(CellStyle)、字体(Font)、行高(RowHeight)等,对于复杂的报表,这几乎是不可能的任务。

使用模板的优势:

  • 格式与数据分离:美工或业务人员可以用 Excel 自身强大的功能设计报表样式,程序员只需关注数据填充。
  • 高效开发:避免编写大量样式设置代码,极大提高开发效率。
  • 易于维护:当报表样式需要调整时,只需修改 .xlsx 模板文件,无需改动 Java 代码。
  • 功能强大:可以轻松实现复杂布局,如合并单元格、图表、图片嵌入等。

准备工作:添加 POI 依赖

如果你使用 Maven,在 pom.xml 中添加以下依赖,注意,POI 5.x 版本对 API 进行了优化,推荐使用。

Java POI如何高效处理Excel模板?-图2
(图片来源网络,侵删)
<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>

模板文件设计:如何创建和使用占位符

  1. 创建一个 Excel 文件 (template.xlsx),用 Excel 软件设计好你想要的布局、样式、标题等。
  2. 定义占位符
    • 简单变量:在需要填充单个数据(如用户名、日期、总计)的单元格中,输入类似 ${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 提供了 XSSFSXSSF 两种 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 引入了 SheetUtilRowUtil,使得操作更直观,但更传统和通用的方法是找到模板行,然后循环复制并填充。

Java POI如何高效处理Excel模板?-图3
(图片来源网络,侵删)
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) ...
}

最佳实践与注意事项

  1. 模板文件是关键:花时间设计一个清晰、易于维护的模板,使用明确的占位符命名(如 ${report.startDate} 而不是 ${date1})。
  2. 处理大数据量:如果列表数据量非常大(例如超过 1 万行),使用 SXSSFWorkbook 会更节省内存,但请注意,SXSSFWorkbook 在写入后就不能再修改之前的行,所以你需要一次性计算好所有数据再写入。
  3. 日期和数字格式:模板中预先设置好单元格的格式(如 yyyy-MM-dd#,##0.00),POI 在替换字符串时不会改变单元格的格式,但如果替换的是数字类型,则依赖你设置的数字格式。
  4. 性能优化:避免在循环中重复创建 CellStyleFont,应该在循环外部创建好,然后在循环中引用它们。
  5. 异常处理:文件操作(IO)和 POI 操作都可能抛出异常,务必使用 try-with-resourcestry-catch-finally 来确保资源(如 FileInputStream, Workbook)被正确关闭。
  6. 测试:用各种边界情况测试你的模板,例如空数据列表、超长文本、特殊字符等,确保生成的 Excel 文件符合预期。

希望这份详细的指南能帮助你熟练掌握 Java POI 操作 Excel 模板!

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