尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Java处理Excel实战:从POI到EasyExcel,解析大文件与内存优化

Java处理Excel实战:从POI到EasyExcel,解析大文件与内存优化 1. 从“Hello World”到“Hello Excel”为什么Java处理表格是个技术活如果你是一个Java开发者无论你是刚入行还是已经写了几年CRUD大概率都遇到过这样一个需求把Excel表格里的数据读出来或者把程序里的数据写进Excel。这听起来像是个简单的“Hello World”级别的任务不就是读个文件嘛。但当你真正上手从搜索引擎里找到一堆“Java读取Excel”的教程然后兴冲冲地复制粘贴代码时往往会发现事情没那么简单。你可能遇到内存溢出OutOfMemoryError因为一个几十兆的Excel文件就把你的服务搞崩了你可能发现日期读出来变成了一个诡异的数字你可能在处理合并单元格时逻辑乱成一团更别提那些带有复杂公式、图表、样式的文件了。这背后是Excel文件格式.xls和.xlsx的复杂性以及不同解析库设计哲学的巨大差异。今天我们不谈那些浅尝辄止的“三行代码搞定”而是从一个有实际项目经验的开发者角度彻底拆解Java读取Excel的几种主流方式讲清楚它们的原理、适用场景以及那些官方文档里不会写的“坑”。这篇文章的目标是让你在下次接到“处理Excel”的需求时不再盲目复制代码而是能根据文件大小、格式复杂度、性能要求自信地选择最合适的工具并写出健壮、高效的代码。我们会重点围绕两个最核心的库——古老的JXL和现在事实上的标准Apache POI——展开同时也会提及其他方案作为补充。关键词很简单Java, Excel, JXL, POI。但背后的水很深。2. 战场地图认识你的敌人——Excel文件格式在拿起任何工具之前你得先了解你要处理的对象。Excel主要有两种文件格式它们决定了你能用什么工具以及可能会遇到什么问题。2.1 旧时代的遗产.xls (BIFF格式).xls是Excel 97-2003使用的二进制文件格式其内部结构基于BIFFBinary Interchange File Format。你可以把它想象成一个结构非常复杂的“二进制数据库”里面用各种记录Record来存储数据、公式、格式等信息。特点二进制格式结构紧凑但规范不公开微软的私有格式。由于是二进制直接解析需要对文件格式有很深的理解。限制最大行数65536行最大列数256列IV。对于现代海量数据处理来说这个上限太低了。解析挑战因为格式不公开早期社区是通过逆向工程来支持它的。这导致某些边缘情况比如一些特殊的单元格格式或老版本的加密可能解析出错。JXL库主要就是针对这个格式的。2.2 现代的标准.xlsx (OOXML格式)从Excel 2007开始微软转向了OOXMLOffice Open XML格式文件扩展名变为.xlsx。这本质上是一个ZIP压缩包。工作原理你可以把一个.xlsx文件的后缀名改为.zip然后用解压软件打开它。你会发现里面是一系列XML文件和一个文件夹结构。例如单元格数据主要存储在xl/worksheets/sheet1.xml这样的文件里样式定义在xl/styles.xml里。优势开放标准OOXML是ECMA和ISO标准文档公开这使得开发支持库变得更容易、更准确。大容量理论支持1048576行2^2016384列XFD。结构清晰因为是XML便于理解和调试。也便于用标准的XML处理工具如SAX进行流式读取这对处理大文件至关重要。解析挑战虽然结构清晰但非常复杂。一个简单的单元格可能涉及多个XML文件中的定义如共享字符串表、样式表。全部加载到内存里消耗巨大。因此如何高效、低内存地解析这些XML是.xlsx处理库的核心课题。注意还有一个.xlsm格式它和.xlsx一样是OOXML压缩包但其中包含了宏VBA代码。用POI处理.xlsm和.xlsx在数据读取上没有本质区别只是需要启用不同的“工作簿”类型来保留宏信息。理解这两种格式的差异是选择工具和优化性能的基础。简单来说如果你的系统只需要兼容老旧的.xls文件JXL可能是一个轻量级选择而面对现代的.xlsx文件尤其是大文件Apache POI的流式APISXSSF/XSSF SAX几乎是唯一的生产环境选择。3. 元老与王者JXL vs. Apache POI 深度对比与选型这是Java处理Excel最经典的两套方案它们的命运也折射了技术栈的变迁。3.1 JXL (Java Excel API)轻量级的黄昏骑士JXL是一个非常老牌的库它的设计目标就是简单、轻量专门用于处理.xls格式。核心特点纯.xls支持只读/写Excel 97-2003的.xls格式。对于.xlsx它无能为力。API简单它的API设计非常直观学习成本低。几行代码就能完成基本的读写。轻量库本身很小依赖少。已停止维护这是最关键的一点。JXL项目已经很长时间没有更新了。在官方源码站上最后版本停留在2.6.122009年。这意味着它不会支持新的Excel特性遇到深层次的Bug也可能无法修复。基本使用示例import jxl.Workbook; import jxl.Sheet; import jxl.Cell; import java.io.File; public class JxlReader { public static void main(String[] args) { try { Workbook workbook Workbook.getWorkbook(new File(test.xls)); Sheet sheet workbook.getSheet(0); // 获取第一个工作表 for (int i 0; i sheet.getRows(); i) { for (int j 0; j sheet.getColumns(); j) { Cell cell sheet.getCell(j, i); // 注意参数顺序列行 System.out.print(cell.getContents() \t); } System.out.println(); } workbook.close(); // 重要必须关闭以释放资源 } catch (Exception e) { e.printStackTrace(); } } }实操心得1API设计注意sheet.getCell(j, i)的参数顺序是(列 行)这和我们的直觉(行 列)是反的非常容易写错。我个人的习惯是在变量命名上就体现出来比如colIndex和rowIndex。实操心得2资源管理Workbook、Sheet等对象持有文件资源必须显式调用close()方法关闭。在JDK 7中强烈建议使用try-with-resources语句自动管理。适用场景与淘汰原因场景维护一个非常古老、且确定只处理.xls格式的系统并且不希望引入复杂的依赖POI。或者在一些资源极其受限的嵌入式环境不过这种环境现在也少了。淘汰原因格式过时、停止维护、功能有限不支持公式求值、复杂的样式和图表等。在新项目中几乎没有理由再选择JXL。3.2 Apache POI企业级的事实标准Apache POI是Apache软件基金会的顶级项目提供了对Microsoft Office格式包括Excel、Word、PowerPoint等的完整读写支持。它是目前Java生态中处理Office文档的绝对主流选择。核心架构与模块POI针对不同的Excel格式提供了不同的API实现类这是理解POI的关键。HSSF (Horrible SpreadSheet Format) 用于处理.xls格式对应JXL的领域。类名以HSSF开头如HSSFWorkbook,HSSFSheet。XSSF (XML SpreadSheet Format) 用于处理.xlsx格式。类名以XSSF开头如XSSFWorkbook,XSSFSheet。这是最常用的API但它是基于DOM的会将整个工作表甚至整个工作簿的XML结构解析并加载到内存对象模型中。SXSSF (Streaming XSSF) 基于XSSF的流式写入API。用于生成非常大的.xlsx文件它通过滑动窗口机制只将一部分行保留在内存中其余写入磁盘临时文件。类名以SXSSF开头。注意SXSSF只优化写入读取仍需用XSSF或SAX事件模型。SAX 事件模型 用于流式读取非常大的.xlsx文件。这不是一套以XSSF开头的类而是使用OPCPackage、XSSFReader并结合Apache的XML解析器如Xerces以SAX方式解析sheet*.xml文件。这是处理海量数据读取的核心方案。为什么POI成为标准功能全面支持.xls和.xlsx的读写、公式、样式、图表、超链接、数据验证等几乎所有Excel特性。持续活跃由Apache社区维护更新频繁紧跟Microsoft Office的新特性。生态强大有丰富的文档、教程和社区问答。几乎所有与Java Excel相关的问题最终都能在POI这里找到答案或解决方案。性能可控虽然默认的XSSF内存消耗大但它提供了SXSSF写和SAX读这两种流式API来应对大数据量场景给了开发者优化空间。选型决策矩阵需求特征推荐方案关键理由处理旧的.xls文件文件小功能简单HSSF (POI)或JXL对于旧格式POI的HSSF更稳定且维护良好。JXL仅在不便引入POI时考虑。处理现代的.xlsx文件文件不大几MB以内XSSF (POI)标准做法API友好功能完整。开发效率高。需要生成非常大的.xlsx文件万行以上SXSSF (POI)流式写入避免内存溢出。这是生产环境导出大数据报表的标配。需要读取非常大的.xlsx文件万行以上POI SAX Event API流式解析XML内存消耗恒定与文件大小无关。学习曲线较陡但必不可少。需要处理公式求值、复杂样式、图表等高级功能XSSF (POI)POI提供了最全面的支持。4. 庖丁解牛使用Apache POI进行精细化操作选定POI作为主力后我们来深入其核心操作。假设我们使用Maven首先需要引入依赖。POI的模块划分很细通常我们引入一个poi-ooxml就会传递引入核心的poi和ooxml-schemas等。dependency groupIdorg.apache.poi/groupId artifactIdpoi-ooxml/artifactId version5.2.3/version !-- 请使用最新稳定版本 -- /dependency4.1 基础读写XSSFWorkbook 的典型用法这是最常见的场景适合处理几MB大小的文件。import org.apache.poi.ss.usermodel.*; import org.apache.poi.xssf.usermodel.XSSFWorkbook; import java.io.FileInputStream; import java.io.FileOutputStream; public class PoiBasicDemo { public static void readExcel(String filePath) throws Exception { // 1. 创建工作簿对象读取 try (FileInputStream fis new FileInputStream(filePath); Workbook workbook new XSSFWorkbook(fis)) { // 自动识别.xlsx .xls请用HSSFWorkbook // 2. 获取工作表 Sheet sheet workbook.getSheetAt(0); // 按索引或 getSheet(Sheet1) // 3. 遍历行和单元格 for (Row row : sheet) { for (Cell cell : row) { // 4. 关键根据单元格类型获取值 CellType cellType cell.getCellType(); Object value; switch (cellType) { case STRING: value cell.getStringCellValue(); break; case NUMERIC: if (DateUtil.isCellDateFormatted(cell)) { // 这是一个日期/时间单元格 value cell.getDateCellValue(); } else { // 这是一个数字 value cell.getNumericCellValue(); } break; case BOOLEAN: value cell.getBooleanCellValue(); break; case FORMULA: // 获取公式字符串 // value cell.getCellFormula(); // 或者获取公式计算后的值需要求值引擎默认可能为缓存值 value cell.getNumericCellValue(); // 假设公式结果是数字 break; case BLANK: value ; break; default: value ; } System.out.print(value \t); } System.out.println(); } } // try-with-resources 自动关闭流和Workbook } public static void writeExcel(String filePath) throws Exception { // 1. 创建工作簿对象写入 try (Workbook workbook new XSSFWorkbook(); FileOutputStream fos new FileOutputStream(filePath)) { // 2. 创建工作表 Sheet sheet workbook.createSheet(我的工作表); // 3. 创建行从0开始 Row headerRow sheet.createRow(0); // 创建单元格并设置值从0开始 headerRow.createCell(0).setCellValue(姓名); headerRow.createCell(1).setCellValue(年龄); headerRow.createCell(2).setCellValue(入职日期); // 4. 创建数据行 Row dataRow sheet.createRow(1); dataRow.createCell(0).setCellValue(张三); dataRow.createCell(1).setCellValue(28); // 设置日期单元格需要创建单元格样式 Cell dateCell dataRow.createCell(2); dateCell.setCellValue(new Date()); // 设置日期值 CellStyle dateStyle workbook.createCellStyle(); CreationHelper createHelper workbook.getCreationHelper(); dateStyle.setDataFormat(createHelper.createDataFormat().getFormat(yyyy-MM-dd)); dateCell.setCellStyle(dateStyle); // 5. 可选自动调整列宽 for (int i 0; i 3; i) { sheet.autoSizeColumn(i); } // 6. 写入文件 workbook.write(fos); } } }踩坑实录1单元格类型判断在POI旧版本如3.x中使用的是Cell.CELL_TYPE_STRING这样的int常量并且getCellType()返回int。在新版本4.0中改为了枚举CellType。混用会导致编译错误。务必查看你使用的POI版本对应的API。踩坑实录2日期与数字的混淆Excel内部将所有数字包括日期存储为NUMERIC类型。日期只是一个有特殊格式的数字从1900/1904年开始的天数。因此判断一个NUMERIC单元格是否是日期必须使用DateUtil.isCellDateFormatted(cell)而不是仅仅依靠单元格的格式字符串去猜。直接对日期单元格调用getNumericCellValue()会得到一个浮点数毫无意义。实操心得3公式处理FORMULA类型的单元格getCellFormula()返回公式字符串如“SUM(A1:A10)”。而getNumericCellValue()或对应的类型方法获取的是上次Excel计算后缓存的结果。如果文件从未被Excel打开计算过或者公式引用了其他未计算的工作表这个值可能是0或错误。POI提供了一个FormulaEvaluator类来在Java端进行公式求值但对于复杂公式或外部引用支持有限性能也有损耗。生产环境中对于包含复杂公式的读操作一种常见的做法是让上游系统或人工先用Excel打开并保存一次确保缓存值是最新的。4.2 性能深渊与逃生通道大文件处理策略当你用上面的XSSFWorkbook代码去读一个包含10万行数据的文件时很可能就会迎来熟悉的java.lang.OutOfMemoryError: Java heap space。原因在于XSSF将整个sheet*.xml文件解析成一个巨大的DOM树放在内存里。解决方案使用SAX事件模型进行流式读取。SAX读取的核心思想是我们不像DOM那样把整个文档吞进内存而是告诉解析器“你一边读一边告诉我你遇到了什么标签事件我来处理”。我们只关心单元格的数据其他如样式等信息可以选择性忽略。import org.apache.poi.openxml4j.opc.OPCPackage; import org.apache.poi.xssf.eventusermodel.XSSFReader; import org.apache.poi.xssf.model.SharedStringsTable; import org.apache.poi.xssf.usermodel.XSSFRichTextString; import org.xml.sax.*; import org.xml.sax.helpers.DefaultHandler; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; import java.io.InputStream; import java.util.Iterator; public class PoiSaxReader { public static void readLargeExcel(String filePath) throws Exception { try (OPCPackage pkg OPCPackage.open(filePath)) { XSSFReader reader new XSSFReader(pkg); // 共享字符串表用于将字符串索引转换为实际字符串 SharedStringsTable sst reader.getSharedStringsTable(); // 获取第一个工作表的XML流 IteratorInputStream sheets reader.getSheetsData(); if (sheets.hasNext()) { InputStream sheetStream sheets.next(); // 创建SAX解析器 SAXParserFactory factory SAXParserFactory.newInstance(); SAXParser parser factory.newSAXParser(); XMLReader xmlReader parser.getXMLReader(); // 设置我们自定义的事件处理器 SheetHandler handler new SheetHandler(sst); xmlReader.setContentHandler(handler); // 开始解析 InputSource sheetSource new InputSource(sheetStream); xmlReader.parse(sheetSource); sheetStream.close(); } pkg.close(); } } // 自定义的SAX事件处理器 private static class SheetHandler extends DefaultHandler { private SharedStringsTable sst; private String lastContents; private boolean nextIsString; private int curRow 0; private int curCol 0; private StringBuilder cellValueBuilder; public SheetHandler(SharedStringsTable sst) { this.sst sst; } Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { // 当解析到单元格c元素时 if (c.equals(qName)) { // 获取单元格引用如“A1”可以解析出行列号这里简化按顺序处理 String cellRef attributes.getValue(r); // 判断单元格数据类型 String cellType attributes.getValue(t); if (s.equals(cellType)) { // 类型为“s”表示是共享字符串表的索引 nextIsString true; } else { nextIsString false; } // 清空值构建器准备接收单元格内容 cellValueBuilder new StringBuilder(); } // 清空上一次的文本内容缓存 lastContents ; } Override public void endElement(String uri, String localName, String qName) throws SAXException { // 单元格内容结束 if (c.equals(qName)) { String value; if (nextIsString cellValueBuilder.length() 0) { // 从共享字符串表中查找真实字符串 int idx Integer.parseInt(cellValueBuilder.toString()); value new XSSFRichTextString(sst.getEntryAt(idx)).toString(); } else { // 直接使用内容数字、布尔值等 value cellValueBuilder.toString(); } // 在这里处理获取到的单元格值(value)和它的位置(curRow, curCol) // 例如存入List、Map或直接处理 System.out.println(Row[ curRow ], Col[ curCol ] value); curCol; } // 一行结束 if (row.equals(qName)) { curRow; curCol 0; System.out.println(--- End of Row (curRow-1) ---); } } Override public void characters(char[] ch, int start, int length) throws SAXException { // 累积单元格内的文本内容 lastContents new String(ch, start, length); if (cellValueBuilder ! null) { cellValueBuilder.append(lastContents); } } } }核心逻辑解析OPCPackage 代表整个.xlsx压缩包。XSSFReader 用于访问包内不同部分如工作表、共享字符串表。SharedStringsTable (SST) 这是.xlsx的一个优化。所有字符串如姓名、地址都集中存储在一个叫sharedStrings.xml的文件里单元格里只存索引。SAX处理器需要引用它来把索引还原成字符串。SheetHandler 继承自DefaultHandler我们重写startElement,endElement,characters方法。当解析器遇到开始标签如c rA1 ts、结束标签/c和文本内容时会回调这些方法。我们在回调中组装数据。巨大优势内存消耗极低只与单行数据的复杂度有关与文件总行数无关。可以轻松处理百万行级别的文件。显著缺点代码复杂需要理解SAX模型和Excel的XML结构。功能受限因为是流式解析你无法随机访问某个单元格比如跳到第50000行只能顺序读取。也很难处理复杂的单元格样式和合并单元格需要额外解析styles.xml和sheet.xml中的其他部分。需要处理共享字符串表增加了逻辑复杂度。重要提示对于超大文件读取SAX是终极解决方案但实现起来繁琐。社区有一些基于POI SAX封装的工具类或开源库如EasyExcel的底层也借鉴了此思路可以简化使用。但在掌握原理之前不建议直接使用黑盒工具。4.3 样式、合并单元格与其他高级特性在实际业务中我们读取的Excel往往不是规整的数据表它可能有表头合并、不同的字体颜色、数字格式等。处理合并单元格合并单元格在POI对象模型中会被“合并”到左上角的那个单元格其他被合并的单元格在Sheet的行列遍历中是不存在的。你需要通过sheet.getNumMergedRegions()和sheet.getMergedRegion(int index)来获取所有合并区域然后在读取时进行判断。// 判断一个单元格是否在合并区域内 boolean isInMergedRegion false; String mergedValue null; for (int i 0; i sheet.getNumMergedRegions(); i) { CellRangeAddress region sheet.getMergedRegion(i); if (region.isInRange(rowIndex, colIndex)) { isInMergedRegion true; // 获取合并区域左上角单元格的值 Row firstRow sheet.getRow(region.getFirstRow()); Cell firstCell firstRow.getCell(region.getFirstColumn()); mergedValue getCellValueAsString(firstCell); // 自定义方法获取值 break; } }踩坑实录如果你按行列遍历遇到被合并的单元格位置会得到null。你必须主动去检查合并区域否则数据会错位。一个常见的做法是先读取所有合并区域信息在遍历单元格时先判断当前位置是否在某个合并区域内如果是则使用该区域左上角的值。读取单元格样式通过Cell.getCellStyle()获取CellStyle对象可以进一步获取字体(getFont)、填充颜色(getFillForegroundColorColor)、数据格式(getDataFormatString)等信息。但要注意样式信息是存储在工作簿级别的单元格只是引用样式索引。CellStyle style cell.getCellStyle(); Font font workbook.getFontAt(style.getFontIndex()); System.out.println(字体是否加粗: font.getBold()); System.out.println(数字格式: style.getDataFormatString()); // 如 yyyy-mm-dd, #,##0.00实操心得直接通过样式索引获取颜色时返回的可能是XSSFColor或HSSFColor需要根据工作簿类型进行类型转换才能拿到RGB值过程稍显繁琐。对于简单的读操作如果不是必须可以暂时忽略样式。5. 超越POI其他方案与最佳实践虽然POI是主流但了解其他选项能让你在特定场景下做出更优选择。5.1 易用性框架EasyExcel这是阿里巴巴开源的一个基于POI的封装框架。它的核心理念是简化开发、节约内存。核心特性注解驱动通过ExcelProperty注解映射Java对象字段和Excel列省去手动解析单元格的繁琐代码。默认大文件支持读和写都默认采用类似SAX的流式模型避免OOM。这是它最大的卖点。监听器模型读文件时实现ReadListener接口每读取一行数据监听器就会收到一个已转换好的Java对象你可以在invoke()方法中处理这一行数据如存入数据库然后丢弃内存中始终只保留少量数据。示例读// 1. 定义数据模型 Data // Lombok注解 public class DemoData { ExcelProperty(姓名) private String name; ExcelProperty(年龄) private Integer age; ExcelProperty(入职日期) private Date hireDate; } // 2. 创建监听器 public class DemoDataListener implements ReadListenerDemoData { // 每隔5条存储数据库实际使用中可以100条然后清理list方便内存回收 private static final int BATCH_COUNT 100; private ListDemoData cachedDataList new ArrayList(); Override public void invoke(DemoData data, AnalysisContext context) { cachedDataList.add(data); if (cachedDataList.size() BATCH_COUNT) { saveData(); // 自定义保存方法 cachedDataList.clear(); } } Override public void doAfterAllAnalysed(AnalysisContext context) { // 最后一批数据保存 saveData(); } private void saveData() { /* 批量插入数据库 */ } } // 3. 执行读取 String fileName large.xlsx; EasyExcel.read(fileName, DemoData.class, new DemoDataListener()).sheet().doRead();适用场景需要快速开发、处理数据模型固定的Excel导入导出尤其是文件较大的场景。它屏蔽了POI的复杂性让开发者更关注业务逻辑。局限性对于高度动态、格式极其不规则比如需要读取任意位置、任意合并的表格的Excel注解映射可能不够灵活需要回退到POI的原生API或使用ListListObject的方式读取。5.2 数据库与ETL工具有时处理Excel的最高效方式不是用Java硬解析。场景一一次性或定期批量导入。如果数据源是Excel目标端是数据库如MySQL完全可以用专业的ETL工具如Kettle, Apache NiFi或者数据库自带工具如MySQL的LOAD DATA INFILE但需要先将Excel转为CSV。这些工具经过高度优化比用Java程序逐行读取、拼SQL插入要快得多也稳定得多。场景二复杂报表生成。与其用POI在代码里艰难地画表格、设置样式不如考虑用专门的报表工具如JasperReports, FineReport。它们提供设计器可以可视化设计模板然后Java程序只需填充数据由引擎生成PDF、Excel等格式质量和效率更高。5.3 实战中的经验结晶依赖管理使用Maven或Gradle管理POI依赖。注意poi-ooxml会传递依赖poi、poi-ooxml-schemas等。确保所有相关依赖版本一致避免因版本冲突导致奇怪的ClassNotFoundException或NoSuchMethodError。资源关闭Workbook、InputStream、OutputStream等对象务必关闭。使用try-with-resources语句是最好也是最安全的方式。异常处理Excel文件可能被用户损坏或者版本不兼容。代码中必须对IOException、InvalidFormatException等进行妥善处理给出友好的错误提示而不是让整个服务崩溃。性能监控在处理大文件时记录开始和结束时间监控内存使用情况如通过Runtime.getRuntime().totalMemory()和freeMemory()。这对于定位性能瓶颈和内存泄漏至关重要。单元测试针对Excel读取逻辑编写单元测试使用不同格式、不同大小、包含错误数据的样例文件进行测试。确保你的代码健壮性。处理“脏数据”用户上传的Excel千奇百怪单元格可能是数字但存成了文本日期格式五花八门有大量空行有隐藏行列。你的程序需要有足够的容错性比如尝试用DataFormatter来统一格式化单元格值为字符串它会根据样式进行格式化或者提供数据清洗和校验的环节。6. 从读取到应用构建健壮的Excel处理组件理解了核心库和原理后我们需要将其封装成易于使用、健壮的组件。以下是一个简化的设计思路你可以根据项目复杂度进行扩展。设计目标提供一个统一的接口根据文件扩展名自动选择解析器HSSF/XSSF/SAX并将解析出的每一行数据转换为一个MapString, Object或自定义的Java Bean同时处理合并单元格和基本样式信息。核心组件ExcelReaderFactory工厂类根据文件类型返回对应的Reader实现。ExcelReader接口定义read(File file, int sheetIndex, RowCallbackHandler handler)等方法。XSSFStreamReader实现ExcelReader接口内部使用POI SAX API实现低内存消耗的顺序读取。XSSFFullReader实现ExcelReader接口内部使用标准的XSSF API用于小文件或需要随机访问、复杂样式的场景。RowCallbackHandler回调接口用户实现此接口来处理每一行解析出的数据ListObject或Map。CellValueResolver负责将POI的Cell对象解析为Java类型的策略类集中处理日期、数字、公式等复杂逻辑。这样做的好处对使用者透明业务开发人员只需关心“拿到数据后做什么”而不需要关心是用SAX还是DOM是.xls还是.xlsx。易于扩展未来如果需要支持新的格式如CSV只需增加新的Reader实现。集中处理复杂性将格式判断、资源管理、异常处理、值解析等脏活累活封装在底层。便于测试和优化可以单独对CellValueResolver或某个Reader进行测试和性能调优。我在实际项目中构建过这样的组件初期投入了一些设计时间但后期在应对各种诡异的Excel文件和处理性能问题时节省了大量的重复劳动和调试时间。特别是将日期解析、空值处理、错误格式转换等逻辑集中到CellValueResolver后整个系统的数据清洗质量得到了保证。最后记住一点处理用户上传的Excel文件永远要抱有最坏的预期——文件可能损坏、格式可能错乱、数据可能包含恶意代码虽然概率低。在服务端一定要对文件大小、扩展名做严格校验解析过程要在独立的线程或甚至有资源限制的环境中进行避免一个坏文件拖垮整个服务。读取Excel从一个简单的功能点出发深入下去涉及了文件格式、解析模型、内存管理、异常处理、API设计等多个方面是一个能很好检验开发者综合能力的课题。希望这篇长文能帮你把这条路看得更清楚一些。
返回列表