当前位置: 首页 > news >正文

FastExcel/EasyExcel简介以及源码解析

简介

官网地址
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

横向对比

jxlpoifastExcel
性能对比效率低细致和完整的操作支持,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;}

http://www.mrgr.cn/news/93256.html

相关文章:

  • 尚庭公寓项目记录
  • AD学习-最小系统板,双层
  • Ubuntu 22.04安装NVIDIA A30显卡驱动
  • Dify+DeepSeek | Excel数据一键可视化(创建步骤案例)(echart助手.yml)(文档表格转图表、根据表格绘制图表、Excel绘制图表)
  • VIA的寄生电感和Stub对高速信号的影响
  • 单细胞分析(21)——SCENIC 分析流程(singularity容器版)
  • RT-thread的MultiButton按键库的使用
  • 记录一次Spring事务失效导致的生产问题
  • 【DeepSeek 】学习编程的利器:DeepSeek 使用指南
  • 由麻省理工学院计算机科学与人工智能实验室等机构创建低成本、高效率的物理驱动数据生成框架,助力接触丰富的机器人操作任务
  • 给没有登录认证的web应用添加登录认证(openresty lua实现)
  • VsCode 快捷键备忘
  • DeepSeek、Grok 和 ChatGPT 对比分析:从技术与应用场景的角度深入探讨
  • ROS系统(三)编程基础
  • Docker 学习(一)
  • ubuntu20系统下conda虚拟环境下安装文件存储位置
  • springBoot文件上传、下载
  • 使用Qt调用HslCommunication(C++调用C#库)
  • P3385 【模板】负环
  • 带你从入门到精通——自然语言处理(五. Transformer中的自注意力机制和输入部分)