简介
官网地址
GitHub地址
基于MIT协议
发展历史
由EasyExcel发展而来
2018/02/07:发布1.0.0
2019/09/17:发布2.0.0
2021/10/21:发布3.0.1
2024/06/18:发布4.0.0
2024/11/06:进入维护模式
2024/12/05:发布FastExcel1.0.0
主要特性
- 高性能读写
- 简单易用
- 流式操作
- 读取执行行数
技术原理
- 内存优化:基于流式读取技术,不需要一次性将整个Excel文件加载到内存中,逐行或逐块读取数据。
- 事件驱动模型:基于实现ReadListener接口处理读取操作。当读取到数据时,会触发接口中的方法,如invoke方法,支持开发者对每行数据进行即时处理。
- 注解映射:用注解将Excel文件中的列与Java对象的属性进行映射。开发者能轻松地将Excel数据转换为Java对象,同时也支持反方向操作,将Java对象写入Excel
横向对比
jxl | poi | fastExcel | |
---|---|---|---|
性能对比 | 效率低 | 细致和完整的操作支持,OOM的问题 | 流式处理机制,仅逐行读写数据,极大地减少了内存消耗 |
API 易用性 | 纯javaAPI,对中文支持非常好,操作简单 | 较为底层和繁琐 | 事件驱动模型,支持自定义注解进行数据映射 |
灵活性与扩展性 | 格式只支持老版本报表 | 能够应对各种定制化需求 | 主要针对常规的读写场景进行了优化 |
源码解析
设计理念
- API友好
- 业务扩展性
- 内存使用率
设计模式
- 建造者模式
- ExcelWriterBuilder
- ExcelWriterSheetBuilder
- ExcelWriterTableBuilder
- ExcelReaderSheetBuilder
- 观察者模式
- ReadListener
- AnalysisEventListener
- 责任链设计模式
- WorkbookHandlerExecutionChain
- SheetHandlerExecutionChain
- RowHandlerExecutionChain
- CellHandlerExecutionChain
- 桥接模式
- ExcelAnalyser
- ExcelReadExecutor
- 工厂模式
- FastExcelFactory
- 模板模式
- CellWriteHandler
- RowTagHandler
- CellTagHandler
- BlankRecordHandler
Read Excel源码解析
// 代码示例
FastExcel.read(new File(fileNameTotal), BaseEntity.class, new UploadDataListener()).sheet().doRead();
Read Excel 初始化
// 运用门面模式,降低使用者的难度,有利于框架的传播
public class FastExcel extends FastExcelFactory {}
// 所有的read()方法都会返回ExcelReaderBuilder
public static ExcelReaderBuilder read(String pathName, Class head, ReadListener readListener) {ExcelReaderBuilder excelReaderBuilder = new ExcelReaderBuilder();excelReaderBuilder.file(pathName);if (head != null) {excelReaderBuilder.head(head);}if (readListener != null) {excelReaderBuilder.registerReadListener(readListener);}return excelReaderBuilder;}
// 所有的sheet()方法都会到这里
// 其中new ExcelReaderSheetBuilder(build())的build()方法会执行前置的初始化操作
public ExcelReaderSheetBuilder sheet(Integer sheetNo, String sheetName) {ExcelReaderSheetBuilder excelReaderSheetBuilder = new ExcelReaderSheetBuilder(build());if (sheetNo != null) {excelReaderSheetBuilder.sheetNo(sheetNo);}if (sheetName != null) {excelReaderSheetBuilder.sheetName(sheetName);}return excelReaderSheetBuilder;}
// 到这里会根据文件类型来进行解析
private void choiceExcelExecutor(ReadWorkbook readWorkbook) throws Exception {
.....// 如果是xlsx格式,会进到这里case XLSX:XlsxReadContext xlsxReadContext = new DefaultXlsxReadContext(readWorkbook, ExcelTypeEnum.XLSX);analysisContext = xlsxReadContext;excelReadExecutor = new XlsxSaxAnalyser(xlsxReadContext, null);break;.....
}
// 会到这个构造方法里 执行readOpcPackage()方法
// 此方法内会调用poi的OPCPackage包来打开excel文件获取文件内每个sheet的数据,包括comment和hyperlink,方便后续进行处理,至此初始化部分已完成public XlsxSaxAnalyser(XlsxReadContext xlsxReadContext, InputStream decryptedStream) throws Exception {this.xlsxReadContext = xlsxReadContext;XlsxReadWorkbookHolder xlsxReadWorkbookHolder = xlsxReadContext.xlsxReadWorkbookHolder();OPCPackage pkg = readOpcPackage(xlsxReadWorkbookHolder, decryptedStream);
.....
}
Read Excel 解析
回到前面sheet()方法,会返回ExcelReaderSheetBuilder
然后调用ExcelReaderSheetBuilder里的doRead()方法
public void doRead() {if (excelReader == null) {throw new ExcelGenerateException("Must use 'FastExcelFactory.read().sheet()' to call this method");}excelReader.read(build());excelReader.finish();}// 最后会调用初始化生成的执行器public void analysis(List<ReadSheet> readSheetList, Boolean readAll) {try {if (!readAll && CollectionUtils.isEmpty(readSheetList)) {throw new IllegalArgumentException("Specify at least one read sheet.");}analysisContext.readWorkbookHolder().setParameterSheetDataList(readSheetList);analysisContext.readWorkbookHolder().setReadAll(readAll);try {excelReadExecutor.execute();..... }
public void execute() {for (ReadSheet readSheet : sheetList) {readSheet = SheetUtils.match(readSheet, xlsxReadContext);if (readSheet != null) {try {xlsxReadContext.currentSheet(readSheet);// 这里传入分页数据以及处理handlerparseXmlSource(sheetMap.get(readSheet.getSheetNo()), new XlsxRowHandler(xlsxReadContext));readComments(readSheet);} catch (ExcelAnalysisStopSheetException e) {if (log.isDebugEnabled()) {log.debug("Custom stop!", e);}}xlsxReadContext.analysisEventProcessor().endSheet(xlsxReadContext);}}}
// 这边会去调用SAX解析,解析时会执行xmlReader.setContentHandler(handler)里的方法
// SAX 解析。SAX 每次解析只在内存中加载 XML 文件的一小部分,即使针对较大的 XML 文件,它也不需要占用太多的内存,也不会存在内存溢出的问题。
// 优点: 1.采用事件驱动模式一段一段的来解析数据,占用内存小 2.只在读取数据时检查数据,不需要保存在内存中 3.效率和性能较高,能解析大于系统内存的文档当然
// 缺点: 1.与 DOM 解析器相比,使用 SAX 解析器读取 XML 文件时,解析逻辑比较复杂 2.同时无法定位文档层次,很难同时访问同一文档的不同部分数据,不支持 XPathprivate void parseXmlSource(InputStream inputStream, ContentHandler handler) {InputSource inputSource = new InputSource(inputStream);try {SAXParserFactory saxFactory;String xlsxSAXParserFactoryName = xlsxReadContext.xlsxReadWorkbookHolder().getSaxParserFactoryName();if (StringUtils.isEmpty(xlsxSAXParserFactoryName)) {saxFactory = SAXParserFactory.newInstance();} else {saxFactory = SAXParserFactory.newInstance(xlsxSAXParserFactoryName, null);}try {saxFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);} catch (Throwable ignore) {}try {saxFactory.setFeature("http://xml.org/sax/features/external-general-entities", false);} catch (Throwable ignore) {}try {saxFactory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);} catch (Throwable ignore) {}SAXParser saxParser = saxFactory.newSAXParser();XMLReader xmlReader = saxParser.getXMLReader();xmlReader.setContentHandler(handler);xmlReader.parse(inputSource);inputStream.close();} catch (IOException | ParserConfigurationException | SAXException e) {throw new ExcelAnalysisException(e);} finally {if (inputStream != null) {try {inputStream.close();} catch (IOException e) {throw new ExcelAnalysisException("Can not close 'inputStream'!");}}}}
我们查看传入的Handler,查看RowTagHandler,继承了AbstractXlsxTagHandler
// 会在操作前后执行动作@Overridepublic void startElement(XlsxReadContext xlsxReadContext, String name, Attributes attributes) {}@Overridepublic void endElement(XlsxReadContext xlsxReadContext, String name) {......// 在这里调用了endRow()方法xlsxReadContext.analysisEventProcessor().endRow(xlsxReadContext);......}
// 会在dealData()方法里处理数据@Overridepublic void endRow(AnalysisContext analysisContext) {if (RowTypeEnum.EMPTY.equals(analysisContext.readRowHolder().getRowType())) {if (LOGGER.isDebugEnabled()) {LOGGER.debug("Empty row!");}if (analysisContext.readWorkbookHolder().getIgnoreEmptyRow()) {return;}}dealData(analysisContext);}
private void dealData(AnalysisContext analysisContext) {ReadRowHolder readRowHolder = analysisContext.readRowHolder();Map<Integer, ReadCellData<?>> cellDataMap = (Map)readRowHolder.getCellMap();readRowHolder.setCurrentRowAnalysisResult(cellDataMap);int rowIndex = readRowHolder.getRowIndex();int currentHeadRowNumber = analysisContext.readSheetHolder().getHeadRowNumber();boolean isData = rowIndex >= currentHeadRowNumber;if (!isData && currentHeadRowNumber == rowIndex + 1) {buildHead(analysisContext, cellDataMap);}// 到这边就会回调监听器生成对象并且到我们自定义的监听器处理数据for (ReadListener readListener : analysisContext.currentReadHolder().readListenerList()) {try {if (isData) {readListener.invoke(readRowHolder.getCurrentRowAnalysisResult(), analysisContext);} else {readListener.invokeHead(cellDataMap, analysisContext);}} catch (Exception e) {onException(analysisContext, e);break;}if (!readListener.hasNext(analysisContext)) {throw new ExcelAnalysisStopException();}}}
点击invoke()到ModelBuildEventListener生成对象监听器里,查看buildUserModel()方法,可以看到此方法在ReadSheetHolder(注解)获取对象信息,通过反射创建对象,然后对各个属性进行赋值,只会在处理到这行数据的时候封装成对应的java对象。
这也就是为什么fastexcel占用内存少的原因
private Object buildUserModel(Map<Integer, ReadCellData<?>> cellDataMap, ReadSheetHolder readSheetHolder,AnalysisContext context) {ExcelReadHeadProperty excelReadHeadProperty = readSheetHolder.excelReadHeadProperty();Object resultModel;try {resultModel = excelReadHeadProperty.getHeadClazz().newInstance();} catch (Exception e) {throw new ExcelDataConvertException(context.readRowHolder().getRowIndex(), 0,new ReadCellData<>(CellDataTypeEnum.EMPTY), null,"Can not instance class: " + excelReadHeadProperty.getHeadClazz().getName(), e);}Map<Integer, Head> headMap = excelReadHeadProperty.getHeadMap();BeanMap dataMap = BeanMapUtils.create(resultModel);for (Map.Entry<Integer, Head> entry : headMap.entrySet()) {Integer index = entry.getKey();Head head = entry.getValue();String fieldName = head.getFieldName();if (!cellDataMap.containsKey(index)) {continue;}ReadCellData<?> cellData = cellDataMap.get(index);Object value = ConverterUtils.convertToJavaObject(cellData, head.getField(),ClassUtils.declaredExcelContentProperty(dataMap, readSheetHolder.excelReadHeadProperty().getHeadClazz(),fieldName, readSheetHolder), readSheetHolder.converterMap(), context,context.readRowHolder().getRowIndex(), index);if (value != null) {dataMap.put(fieldName, value);}}return resultModel;}