1. 从“能打开”到“读明白”:POI读取Excel的实战起点

最近在后台看到不少朋友在搜Java POI读取Excel,特别是 .xlsx 文件。这确实是个高频需求,无论是处理业务报表、导入用户数据,还是做数据清洗,都绕不开它。很多人上手的第一感觉是:“这不就是几行代码的事吗?” 确实,网上随便一搜,都能找到用 XSSFWorkbook 打开文件、遍历 Sheet Row 的示例代码。但真到了自己动手,尤其是面对格式复杂、数据量稍大的实际文件时,各种问题就冒出来了:内存溢出(OOM)、日期数字读出来是浮点数、合并单元格处理不当导致数据错位、空行空列判断失误…… 这些坑,我都一个不落地踩过。

所以,今天我们不聊那些“Hello World”级别的入门代码。我想和你深入聊聊,在真实的项目环境中,如何稳健、高效且正确地使用Apache POI的 XSSFWorkbook 来读取 .xlsx 文件。这不仅仅是调用API,更是一套包含环境搭建、核心对象理解、数据精准提取、性能优化和异常处理在内的完整解决方案。无论你是正在处理一个紧急的数据导入需求,还是想系统性地掌握POI,这篇文章都能给你提供可直接“抄作业”的实践指南。

2. 环境准备与核心依赖:别在第一步就卡住

在开始写代码之前,正确的环境配置是基石。很多初学者遇到的第一个拦路虎往往是依赖问题或环境冲突。

2.1 依赖引入:选对版本,避免冲突

对于Maven项目,在 pom.xml 中添加POI依赖是最常见的方式。但这里有个关键点:Apache POI是一个模块化的项目,我们需要引入一组相关的依赖,而不仅仅是一个 poi 的JAR包。

<dependencies>
    <!-- POI核心库 -->
    <dependency>
        <groupId>org.apache.poi</groupId>
        <artifactId>poi</artifactId>
        <version>5.2.3</version> <!-- 建议使用较新稳定版 -->
    </dependency>
    <!-- 处理.xlsx格式的OOXML实现 -->
    <dependency>
        <groupId>org.apache.poi</groupId>
        <artifactId>poi-ooxml</artifactId>
        <version>5.2.3</version>
    </dependency>
    <!-- 处理OOXML的Schemas,某些复杂操作需要 -->
    <dependency>
        <groupId>org.apache.poi</groupId>
        <artifactId>poi-ooxml-schemas</artifactId>
        <version>4.1.2</version>
    </dependency>
</dependencies>

注意 :务必保持 poi poi-ooxml 的版本一致,否则可能会引发难以排查的 NoClassDefFoundError ClassNotFoundException ,比如热词里提到的 NoClassDefFoundError: org/apache/poi/POIXMLDocumentPart ,很多时候就是版本不匹配导致的。 poi-ooxml-schemas 的版本可以独立,但建议使用与核心库兼容的版本。

如果你用的是Gradle,对应的依赖声明如下:

implementation 'org.apache.poi:poi:5.2.3'
implementation 'org.apache.poi:poi-ooxml:5.2.3'
implementation 'org.apache.poi:poi-ooxml-schemas:4.1.2'

2.2 理解HSSF与XSSF:老司机与新高速

Apache POI提供了两套主要的API来处理Excel:

  • HSSF (Horrible SpreadSheet Format) : 用于处理旧的 .xls 格式(Excel 97-2003)。它是基于二进制流的,在处理超大文件时有限制(行数约65535,列数256)。
  • XSSF (XML SpreadSheet Format) : 用于处理新的 .xlsx 格式(Excel 2007+)。它基于OOXML(Office Open XML)标准,本质上是一个ZIP压缩包,里面包含了一系列XML文件。它支持更大的数据量(行数超过100万,列数16384),但内存消耗通常比HSSF高。

我们的主题是 .xlsx ,所以核心类就是 XSSFWorkbook 。它代表整个Excel工作簿,是我们所有操作的入口。

2.3 基础读取代码骨架:先让程序跑起来

让我们先写一个最基础的、能跑通的读取示例,建立直观感受:

import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import java.io.FileInputStream;
import java.io.IOException;

public class BasicExcelReader {
    public static void main(String[] args) {
        // 文件路径,请替换为你的实际文件路径
        String filePath = "your_file.xlsx";

        // 使用try-with-resources确保资源自动关闭,避免内存泄漏
        try (FileInputStream fis = new FileInputStream(filePath);
             Workbook workbook = new XSSFWorkbook(fis)) { // 关键:创建XSSFWorkbook对象

            // 1. 获取第一个工作表(Sheet)
            Sheet sheet = workbook.getSheetAt(0);

            // 2. 遍历每一行(Row)
            for (Row row : sheet) {
                // 3. 遍历行中的每一个单元格(Cell)
                for (Cell cell : row) {
                    // 4. 根据单元格类型读取数据
                    switch (cell.getCellType()) {
                        case STRING:
                            System.out.print(cell.getStringCellValue() + "\t");
                            break;
                        case NUMERIC:
                            if (DateUtil.isCellDateFormatted(cell)) {
                                System.out.print(cell.getDateCellValue() + "\t");
                            } else {
                                System.out.print(cell.getNumericCellValue() + "\t");
                            }
                            break;
                        case BOOLEAN:
                            System.out.print(cell.getBooleanCellValue() + "\t");
                            break;
                        case FORMULA:
                            System.out.print(cell.getCellFormula() + "\t");
                            break;
                        default:
                            System.out.print("[空或未知]\t");
                    }
                }
                System.out.println(); // 换行,表示一行结束
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

这段代码构成了读取Excel的骨架。但如果你直接拿它去读一个复杂的生产环境文件,大概率会出问题。接下来,我们就逐一拆解这里面每一个环节可能遇到的“坑”和最佳实践。

3. 核心对象模型与数据提取:精准获取每一个值

POI的对象模型与Excel的结构是对应的: Workbook -> Sheet -> Row -> Cell 。理解每一层的特性和API是精准提取数据的前提。

3.1 Sheet的获取与遍历:不只是getSheetAt(0)

workbook.getSheetAt(0) 获取的是索引为0(第一个)的工作表。但在实际文件中,我们可能需要按名称获取,或者遍历所有工作表。

// 按名称获取Sheet(更常用,更稳定)
Sheet targetSheet = workbook.getSheet("订单数据");
if (targetSheet == null) {
    throw new IllegalArgumentException("未找到名为'订单数据'的工作表!");
}

// 遍历所有Sheet
for (Sheet sheet : workbook) {
    System.out.println("正在处理Sheet: " + sheet.getSheetName());
    // ... 处理每个sheet的数据
}

遍历行(Row)的注意事项 : POI的 Sheet 实现了 Iterable<Row> ,所以可以用 for (Row row : sheet) 来遍历。但要注意, 这个迭代器只会遍历有数据的行(即创建过的行) 。如果Excel中第1、3、5行有数据,第2、4行是空的(完全未编辑过),那么迭代器只会返回第1、3、5行。 sheet.getLastRowNum() 返回的是最后一个有数据的行的索引(从0开始), sheet.getPhysicalNumberOfRows() 返回的是有数据的行的实际数量。

3.2 Cell类型判断与值获取:避免ClassCastException的陷阱

上面基础代码中的 switch (cell.getCellType()) 在POI 3.17版本后已经过时。从POI 4.0开始,推荐使用 getCellType() 返回 CellType 枚举,并结合 getCellType() 方法来判断。这是代码健壮性的关键。

import org.apache.poi.ss.usermodel.CellType;

// ... 在遍历Cell的循环内
CellType cellType = cell.getCellType();
switch (cellType) {
    case STRING:
        String strValue = cell.getStringCellValue();
        // 处理前可trim()去除首尾空格
        break;
    case NUMERIC:
        // 难点:NUMERIC类型既可能是数字,也可能是日期
        if (DateUtil.isCellDateFormatted(cell)) {
            // 是日期
            Date dateValue = cell.getDateCellValue();
            // 使用SimpleDateFormat或Java 8+的DateTimeFormatter格式化
        } else {
            // 是纯数字
            double numValue = cell.getNumericCellValue();
            // 注意:Excel中存储的整数(如100)读出来也是double(100.0)
            // 如果需要整数,可以强转或判断
            if (numValue == Math.floor(numValue) && !Double.isInfinite(numValue)) {
                long intValue = (long) numValue;
            }
        }
        break;
    case BOOLEAN:
        boolean boolValue = cell.getBooleanCellValue();
        break;
    case FORMULA:
        // 公式单元格:可以获取公式字符串,或计算后的值
        String formula = cell.getCellFormula();
        // 获取公式计算后的值(注意:POI默认不一定计算,可能需要evaluate)
        FormulaEvaluator evaluator = workbook.getCreationHelper().createFormulaEvaluator();
        CellValue formulaValue = evaluator.evaluate(cell);
        // 然后根据formulaValue的cellType再处理
        break;
    case BLANK:
        // 单元格存在但内容为空
        break;
    case ERROR:
        byte errorCode = cell.getErrorCellValue();
        break;
    default:
        // _NONE或未知类型
        break;
}

一个极其常见的坑:空单元格的处理 如果某个单元格从未被编辑过,通过 row.getCell(columnIndex) 获取到的可能是 null 。直接对其调用 getCellType() 会抛出 NullPointerException 。安全的写法是:

Cell cell = row.getCell(j, Row.MissingCellPolicy.CREATE_NULL_AS_BLANK);
// 或者
Cell cell = row.getCell(j);
if (cell == null) {
    // 按空值处理
    value = "";
} else {
    // 正常读取
    value = ...;
}

使用 Row.MissingCellPolicy.CREATE_NULL_AS_BLANK 策略,即使单元格不存在,也会返回一个 CellType BLANK 的单元格对象,避免了空指针,简化了逻辑。

3.3 处理特殊单元格:合并单元格、样式与公式

合并单元格 :这是数据错位的重灾区。比如一个标题跨了A1到D1,如果你遍历第1行的A、B、C、D列,会发现只有A1有值,B1、C1、D1都是空或 null 。POI提供了 sheet.getMergedRegions() 来获取所有合并区域。

List<CellRangeAddress> mergedRegions = sheet.getMergedRegions();
for (CellRangeAddress mergedRegion : mergedRegions) {
    int firstRow = mergedRegion.getFirstRow();
    int lastRow = mergedRegion.getLastRow();
    int firstCol = mergedRegion.getFirstColumn();
    int lastCol = mergedRegion.getLastColumn();
    // 通常,只有左上角(firstRow, firstCol)的单元格有值
    // 在遍历时,如果当前单元格(rowIndex, colIndex)位于某个合并区域内,且不是左上角,则应取左上角的值
}

在实际处理中,我通常会先遍历所有合并区域,建立一个快速查找的映射(例如,用一个 Map<String, Cell> ,key是 “行-列” ,value是合并区域左上角单元格),在读取每个单元格时先查这个映射。

单元格样式 :通过 cell.getCellStyle() 可以获取字体、颜色、对齐方式、数据格式等。 cell.getCellStyle().getDataFormatString() 可以拿到Excel中设置的格式代码(如“yyyy-mm-dd”、“0.00%”),这对于判断数字的显示格式非常有用。

公式计算 :如前所述,对于 FORMULA 类型的单元格,如果需要获取计算结果,必须使用 FormulaEvaluator 。对于 .xlsx ,对应的类是 XSSFFormulaEvaluator 。注意,如果Excel文件是在不依赖Excel应用的环境下生成的,某些复杂公式可能无法被POI计算。

4. 性能优化与内存管理:应对大数据文件的挑战

XSSFWorkbook 在内存中维护了整个Excel文档的DOM树,对于几兆几十兆的文件没问题,但遇到几十上百列、几十万行数据的文件,很容易导致JVM堆内存溢出(OOM)。这是使用POI处理 .xlsx 最严峻的挑战。

4.1 使用SXSSF(流式API)进行读取

POI提供了 SXSSF (Streaming Usermodel API)用于写入超大Excel文件,但 请注意,SXSSF本身并不直接提供对应的流式读取API 。对于读取,POI提供了另一种模式: 事件模型(EventModel) ,对应 XSSF SAX (Simple API for XML)解析器。这是处理超大文件读取的正确姿势。

核心类是 org.apache.poi.xssf.eventusermodel.XSSFReader org.apache.poi.xssf.eventusermodel.ReadOnlySharedStringsTable 。它的原理类似于SAX解析XML,不需要将整个文件加载到内存,而是边读边处理,内存中只保留当前正在处理的部分数据。

下面是一个简化的事件模型读取示例框架:

import org.apache.poi.openxml4j.opc.OPCPackage;
import org.apache.poi.xssf.eventusermodel.XSSFReader;
import org.apache.poi.xssf.eventusermodel.XSSFReader.SheetIterator;
import org.apache.poi.xssf.model.SharedStringsTable;
import org.xml.sax.InputSource;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.XMLReaderFactory;
import java.io.InputStream;

public class LargeExcelReader {
    public void processLargeFile(String filePath) throws Exception {
        try (OPCPackage pkg = OPCPackage.open(filePath)) {
            XSSFReader reader = new XSSFReader(pkg);
            SharedStringsTable sst = new SharedStringsTable(pkg); // 处理共享字符串
            // 获取样式(如果需要)
            // StylesTable styles = reader.getStylesTable();

            // 自定义的SAX事件处理器,这是核心
            MySheetHandler handler = new MySheetHandler(sst);

            XMLReader parser = XMLReaderFactory.createXMLReader();
            parser.setContentHandler(handler);

            // 遍历所有Sheet
            SheetIterator sheets = (SheetIterator) reader.getSheetsData();
            while (sheets.hasNext()) {
                InputStream sheetStream = sheets.next();
                String sheetName = sheets.getSheetName();
                System.out.println("Processing Sheet: " + sheetName);
                handler.setSheetName(sheetName); // 告诉处理器当前sheet名
                InputSource sheetSource = new InputSource(sheetStream);
                parser.parse(sheetSource); // 开始SAX解析
                sheetStream.close();
            }
        }
    }

    // 你需要实现一个继承自org.xml.sax.helpers.DefaultHandler的类
    // 在startElement, characters, endElement等方法中解析单元格数据
    static class MySheetHandler extends DefaultHandler {
        private SharedStringsTable sst;
        private String sheetName;
        private StringBuilder cellValue;
        private boolean isString;
        // ... 其他状态变量,用于追踪当前行、列、单元格类型等
        public MySheetHandler(SharedStringsTable sst) { this.sst = sst; }
        public void setSheetName(String name) { this.sheetName = name; }
        @Override
        public void startElement(String uri, String localName, String qName, Attributes attributes) {
            // 解析到 <row> 标签,开始新的一行
            // 解析到 <c> 标签,开始一个新的单元格,从attributes中获取单元格类型(t属性)和引用(r属性,如A1)
            // 如果 t="s",说明是共享字符串索引,将isString置为true
            // 如果 t="n",是数字;t="b",是布尔;t="str",是内联字符串;t="e",是错误
            // 初始化cellValue StringBuilder
        }
        @Override
        public void characters(char[] ch, int start, int length) {
            // 累积单元格内的文本内容
            if (cellValue != null) {
                cellValue.append(ch, start, length);
            }
        }
        @Override
        public void endElement(String uri, String localName, String qName) {
            // 解析到 </c> 标签,一个单元格结束。根据isString和cellValue,从sst中获取实际字符串或直接使用数字。
            // 将解析出的单元格数据存储到你定义的数据结构(如List<Map>)中。
            // 解析到 </row> 标签,一行结束。可以将整行数据批量处理(如存入数据库)。
        }
    }
}

事件模型代码更复杂,但它是处理百MB甚至GB级别Excel文件的唯一可行方案。如果你的文件不大(例如小于50MB),使用标准的 XSSFWorkbook 并配合以下技巧更简单。

4.2 标准XSSFWorkbook的优化技巧

如果坚持使用 XSSFWorkbook ,可以尝试这些优化:

  1. 设置JVM堆内存 :通过启动参数 -Xmx2048m -Xmx4096m 增加最大堆内存。
  2. 及时关闭资源 :使用 try-with-resources 确保 Workbook FileInputStream 被关闭,释放资源。
  3. 避免频繁创建对象 :例如, DataFormatter FormulaEvaluator 可以在循环外创建并复用。
  4. 按需读取 :如果只需要特定Sheet或特定区域的数据,不要遍历整个文件。用 sheet.getRow() 按索引获取特定行,而不是遍历所有行。
  5. 处理完即丢弃 :对于超大文件,在读取并处理完一行或一批数据后,可以尝试将对应的Java对象(如 Row Cell )的引用置为 null ,虽然POI内部缓存可能不会立即释放,但有助于GC。
  6. 考虑文件拆分 :如果可能,在业务上游将大文件拆分成多个小文件处理。

5. 实战中的疑难杂症与解决方案

掌握了基础和性能,我们来看看那些让人头疼的具体问题。

5.1 日期与数字的“傻傻分不清”

这是最高频的问题。Excel内部将所有日期和时间存储为数字(自1900年或1904年日期系统以来的天数加上小数部分)。POI读取时,如果单元格格式是日期格式, DateUtil.isCellDateFormatted(cell) 会返回 true

坑点1:格式丢失 。如果Excel中一个单元格看起来是“2023-10-27”,但实际格式是“常规”或“文本”,POI会将其读为数字(如45214)。解决方案是 优先依赖Excel的格式信息

// 更健壮的日期判断
if (cell.getCellType() == CellType.NUMERIC) {
    CellStyle style = cell.getCellStyle();
    String dataFormat = style.getDataFormatString();
    // 判断格式字符串是否包含日期元素
    if (DateUtil.isCellDateFormatted(cell) ||
        dataFormat.contains("yy") || dataFormat.contains("mm") || dataFormat.contains("dd") ||
        dataFormat.contains("h") || dataFormat.contains("s")) {
        // 尝试作为日期解析
        Date date = cell.getDateCellValue();
        // 使用DataFormatter可以按单元格格式化为字符串,更接近Excel显示的样子
        DataFormatter formatter = new DataFormatter();
        String dateAsString = formatter.formatCellValue(cell);
    } else {
        // 作为数字处理
        double num = cell.getNumericCellValue();
    }
}

坑点2:1904日期系统 。Mac版Excel默认使用1904日期系统(基准日期是1904年1月1日),而Windows版默认使用1900系统。POI的 DateUtil 默认使用1900系统。如果文件来自Mac,日期可能会差4年零1天。可以通过 workbook.isDate1904() 判断,并进行校正。

5.2 公式单元格的取值策略

对于 FORMULA 类型的单元格,你有几种选择:

  • cell.getCellFormula() :获取公式字符串,如“SUM(A1:A10)”。
  • cell.getNumericCellValue() / cell.getStringCellValue() 直接获取缓存的计算结果 。如果Excel文件被POI打开前已经计算并保存了,这里能拿到值。否则可能得到默认值或抛出异常。
  • 使用 FormulaEvaluator.evaluate(cell) 让POI重新计算公式 。这是最可靠的方式,但性能开销大,且POI支持的函数有限。
FormulaEvaluator evaluator = workbook.getCreationHelper().createFormulaEvaluator();
CellValue cellValue = evaluator.evaluate(cell);
switch (cellValue.getCellType()) {
    case NUMERIC:
        double num = cellValue.getNumberValue();
        break;
    case STRING:
        String str = cellValue.getStringValue();
        break;
    // ... 处理其他类型
}

提示 :对于大量公式单元格,建议在读取前,让用户在Excel中手动保存一次(确保公式结果已计算缓存),或者评估使用 FormulaEvaluator 的性能影响。对于非常复杂的公式或外部链接,POI可能无法计算。

5.3 自定义数据格式与富文本处理

Excel单元格可以设置自定义格式,如“0.00%”、“#,##0.00”、“yyyy年mm月dd日”。使用 DataFormatter 可以按照单元格的格式将值格式化成字符串,这是最接近Excel显示效果的方式。

DataFormatter formatter = new DataFormatter();
// 设置区域,影响日期/数字格式(可选)
// formatter.setDefaultNumberFormat(new java.text.DecimalFormat("#,##0.00"));
String formattedValue = formatter.formatCellValue(cell);
// 对于公式单元格,formatter会先尝试计算
String formattedFormulaValue = formatter.formatCellValue(cell, evaluator);

对于单元格内包含富文本(部分文字加粗、变色), cell.getRichStringCellValue() 返回的是 XSSFRichTextString ,它包含了所有格式信息。 cell.getStringCellValue() 只返回纯文本。

5.4 空行、隐藏行与筛选状态的处理

  • 空行判断 :不要只判断 row == null 。一行可能被创建但所有单元格都为空。更准确的判断是遍历该行所有单元格,检查是否都为空白或空。
    public boolean isRowEmpty(Row row) {
        if (row == null) return true;
        for (int c = row.getFirstCellNum(); c < row.getLastCellNum(); c++) {
            Cell cell = row.getCell(c);
            if (cell != null && cell.getCellType() != CellType.BLANK) {
                return false;
            }
        }
        return true;
    }
    
  • 隐藏行/列 row.getZeroHeight() 返回 true 表示行被隐藏。 sheet.isColumnHidden(columnIndex) 判断列是否隐藏。
  • 筛选状态 :POI可以读取筛选区域( sheet.getAutoFilter() ),但判断某行是否被筛选掉需要更复杂的逻辑,通常需要结合单元格值进行业务判断。

6. 封装与工具类:构建可复用的读取组件

在实际项目中,我们很少会每次都写一遍完整的解析循环。封装一个健壮、可配置的Excel读取工具类是更佳实践。这个工具类应该考虑:

  1. 配置化 :允许指定要读取的Sheet(名称或索引)、起始行(跳过表头)、要读取的列索引等。
  2. 数据类型映射 :定义如何将单元格值转换为Java类型(String, Integer, Double, Date, Boolean等)。
  3. 数据校验 :在读取时进行简单的非空、格式、范围校验。
  4. 批处理与回调 :支持每读取一行或一批数据,通过回调函数进行处理(如存入数据库),避免在内存中累积所有数据。
  5. 异常处理与日志 :清晰地记录解析过程中的错误(如某行某列格式错误),是跳过整行还是记录错误继续,需要有策略。

下面是一个高度简化的工具类设计思路:

public class ExcelReader<T> {
    public interface RowMapper<T> {
        T mapRow(int rowIndex, List<Object> cellValues);
    }

    public List<T> read(String filePath, int sheetIndex, int startRow, RowMapper<T> rowMapper) throws IOException {
        List<T> result = new ArrayList<>();
        try (Workbook workbook = WorkbookFactory.create(new File(filePath))) {
            Sheet sheet = workbook.getSheetAt(sheetIndex);
            DataFormatter formatter = new DataFormatter();
            FormulaEvaluator evaluator = workbook.getCreationHelper().createFormulaEvaluator();

            for (int i = startRow; i <= sheet.getLastRowNum(); i++) {
                Row row = sheet.getRow(i);
                if (isRowEmpty(row)) continue;

                List<Object> rowData = new ArrayList<>();
                for (int j = 0; j < row.getLastCellNum(); j++) {
                    Cell cell = row.getCell(j, Row.MissingCellPolicy.CREATE_NULL_AS_BLANK);
                    Object value = getCellValue(cell, formatter, evaluator); // 封装好的取值方法
                    rowData.add(value);
                }
                T obj = rowMapper.mapRow(i, rowData);
                if (obj != null) {
                    result.add(obj);
                }
            }
        }
        return result;
    }
    // ... 实现 isRowEmpty, getCellValue 等方法
}

使用方式:

List<Order> orders = reader.read("orders.xlsx", 0, 1, (rowIdx, cellVals) -> {
    if (cellVals.size() < 5) return null; // 跳过列数不足的行
    Order order = new Order();
    order.setOrderId((String)cellVals.get(0));
    order.setAmount(((Double)cellVals.get(1)).doubleValue());
    // ... 其他字段映射,注意类型转换异常处理
    return order;
});

7. 从读取到应用:结合业务场景的思考

掌握了技术细节,最终还是要为业务服务。不同的场景对Excel读取的要求截然不同。

  • 数据导入 :强调 准确性和健壮性 。需要严格的校验(格式、必填、唯一性、业务逻辑),清晰的错误报告(哪一行哪一列有什么问题),以及事务性(全部成功或全部回滚,或部分成功部分失败记录)。通常需要先读取到内存列表,整体校验后再批量入库。
  • 报表解析 :可能更关注 特定区域的数据 (如某个命名区域 NamedRange ),需要处理复杂的 合并单元格和表格结构 。POI可以通过 sheet.getMergedRegions() workbook.getName(“RangeName”) 来定位。
  • 数据核对与清洗 :需要 高性能地遍历和比较 。事件模型(SAX)在这里可能是必须的。同时,可能需要将读取的数据与数据库或其他来源的数据进行比对。
  • 模板填充 :读取可能只是第一步,更多是定位到模板中的占位符,然后用POI的写入API( XSSFWorkbook , Cell.setCellValue )将新数据填进去。

无论哪种场景,在开始编码前,花时间分析Excel文件的结构(用Excel软件打开,查看Sheet、表头、数据区域、格式、公式、合并单元格),设计好对应的Java数据模型(POJO)和映射规则,往往能事半功倍,减少后期调试的麻烦。记住,处理Excel文件,一半是技术,另一半是对数据本身的理解。

更多推荐