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

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);
                    // 这里传入分页数据以及处理handler
                    parseXmlSource(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.同时无法定位文档层次,很难同时访问同一文档的不同部分数据,不支持 XPath
    private 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

// 会在操作前后执行动作
    @Override
    public void startElement(XlsxReadContext xlsxReadContext, String name, Attributes attributes) {

    }

    @Override
    public void endElement(XlsxReadContext xlsxReadContext, String name) {
    ......
    // 在这里调用了endRow()方法
    xlsxReadContext.analysisEventProcessor().endRow(xlsxReadContext);
	......
    }
	// 会在dealData()方法里处理数据
    @Override
    public 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.kler.cn/a/573443.html

相关文章:

  • 张岳教授:语言模型推理与泛化研究 | ICLR 2025 特邀报告与团队专场
  • 18类创新平台培育入库!长沙经开区2025年各类科技创新平台培育申报流程时间材料及申报条件
  • jQuery UI 简介
  • 【漫话机器学习系列】119.小批量随机梯度方法
  • 机器学习中的优化方法:从局部探索到全局约束
  • Measuring short-form factuality in large language models (SimpleQA) 论文简介
  • mybatis日期格式与字符串不匹配bug
  • 解锁前端表单数据的秘密旅程:从后端到用户选择!✨
  • 微服务通信:用gRPC + Protobuf 构建高效API
  • Java+SpringBoot+Vue+数据可视化的百草园化妆服务平台(程序+论文+讲解+安装+调试+售后)
  • 年后寒假总结及计划安排
  • linux安装Kafka以及windows安装Kafka和常见问题解决
  • 迷你世界脚本对象库接口:ObjectLib
  • Oracle CBD结构和Non-CBD结构区别
  • 微软官宣5 月 5 日关闭 Skype,赢者通吃法则依然有效
  • 解锁网络防御新思维:D3FEND 五大策略如何对抗 ATTCK
  • 如何快速的用pdfjs建立一个网页可以在线阅读你的PDF文件
  • 加密算法学习与SpringBoot实践
  • Java 多态:代码中的通用设计模式
  • 第七节:基于Winform框架的串口助手小项目---协议解析《C#编程》