• 🏆【Alibaba中间件技术系列】「EasyExcel实战案例」实战研究一下EasyExcel如何从指定文件位置进行读取数据


    EasyExcel的使用背景

    工作中总会遇到对Excel读写功能,之前接触过EasyExcel,后续我们基本上用它代替了传统的POI和JXL、甚至还有一个EasyPOI技术。

    EasyExcel的时候痛点

    使用的EasyExcel时候,一般场景下表头比较传统,也不复杂,但是这次呢表头稍微有点复杂,读取数据要从指定的位置开始,要从指定位置开始读取EasyExcel,所以呢在不断的摸索之后,找到了合适的解决方法。

    EasyExcel对比其他框架

    平常用poi读取excel数据量少,加上EasyExcel读取Excel有点复杂,所以一直也没在项目中使用EasyExcel,直到有一回要读取的数据量太大,使用poi读取Excel在创建Workbook -> WorkbookFactory.create(inputStream) 时就异常了,分配很多内存也不好使,所以放弃使用poi转使用EasyExcel。

    Java解析、生成Excel比较有名的框架有Apache poi、jxl。但他们都存在一个严重的问题就是非常的耗内存,poi有一套SAX模式的API可以一定程度的解决一些内存溢出的问题,但POI还是有一些缺陷,比如07版Excel解压缩以及解压后存储都是在内存中完成的,内存消耗依然很大。easyexcel重写了poi对07版Excel的解析,能够原本一个3M的excel用POI sax依然需要100M左右内存降低到几M,并且再大的excel不会出现内存溢出,03版依赖POI的sax模式。

    在上层做了模型转换的封装,让使用者更加简单方便 --EasyExcel
    使用EasyExcel读取Excel时一直在想如何简化读取方式,不用读取每个Excel都创建一个XXDataListene监听器类,刚开始想,把DataListener加上泛型,共用一个DataListener,但是还涉及到如何传递Dao和每个Dao如何保存数据,而且保存数据前可能还需要对数据进行不同的处理。

    EasyExcel的编程模式

    EasyExcel开源挺久了,但使用上感觉有点让人望而生怯,刚开始看官方文档上读取Excel挺简单的,只需要一行代码,继续细看的话还需要创建一个回调监听器,有点复杂呀(每个Excel都需要创建一个单独的回调监听器类)。

    EasyExcel读取的指定位置

    要开始读取数据,第8行才是真正的数据,直接上代码,headRowNumber(),不写默认是1,即就是从第二行开始读数据。

        /**
         * 读取文件信息数据
         * @param filePath
         * @param headNum
         */
        public ContactInfoExcelDataListener read(String filePath , int headNum){
            EasyExcel.read(filePath, this).head(ContactInfoExcelEntity.class).autoCloseStream(true
                    ).autoTrim(true).ignoreEmptyRow(true).sheet()
                    // 这里可以设置1,因为头就是一行。如果多行头,可以设置其他值。不传入也可以,因为默认会根据DemoData 来解析,他没有指定头,也就是默认1行
                    .headRowNumber(Math.max(headNum,NumberUtils.BYTE_ZERO)).doRead();
            return this;
        }
    
        /**
         * 读取文件信息数据
         * @param filePath
         */
        public ContactInfoExcelDataListener read(String filePath){
            EasyExcel.read(filePath, this).head(ContactInfoExcelEntity.class).autoCloseStream(true).autoTrim(true).ignoreEmptyRow(true).sheet()
                    // 这里可以设置1,因为头就是一行。如果多行头,可以设置其他值。不传入也可以,因为默认会根据DemoData 来解析,他没有指定头,也就是默认1行
                   .doRead();
            return this;
        }
    
        /**
         * 读取文件信息数据
         * @param inputStream
         * @param headNum
         */
        public ContactInfoExcelDataListener read(InputStream inputStream, int headNum){
            EasyExcel.read(inputStream, this).head(ContactInfoExcelEntity.class).autoCloseStream(true).autoTrim(true).ignoreEmptyRow(true).sheet()
                    // 这里可以设置1,因为头就是一行。如果多行头,可以设置其他值。不传入也可以,因为默认会根据DemoData 来解析,他没有指定头,也就是默认1行
                    .headRowNumber(Math.max(headNum,NumberUtils.BYTE_ZERO)).doRead();
            return this;
        }
    

    导入数据的流程

    表头校验
    invokeHeadMap()方法
    
        /**
         * 调用头部
         * @param map
         * @param analysisContext
         */
        @Override
        public void invokeHead(Map<Integer, CellData> map, AnalysisContext analysisContext) {
            log.info("【start read the excel head data】:{}",map);
            // 判断标记头是否存在
            //基本都会走到这里,全部放权交接给invoke方法,并且巧用作为我们锁初始化操作的控制赋值,切记如果headNum = 0 此方法很有可能不会触发,慎用!
            //一次性筷子!赋值为1,目前只是实现了相关的单节点同步锁,如果未来扩展了相关的分布式节点,需要采用分布式锁机制进行控制!锁范围需要进行控制
            try {
                int titleRows = map.size();
                // 头部的中断处理机制!
                failureDataCount = preValidate?orginalHead.size() != titleRows?NumberUtils.INTEGER_ONE:
                        NumberUtils.BYTE_ZERO:NumberUtils.BYTE_ZERO;
                // 进行置位
                if(preValidate && (failureDataCount.intValue() == NumberUtils.INTEGER_ONE)){
                    causeByHeadFormatAbort = Boolean.TRUE;
                }
                if(!isMockFlag) {
                    // TODO 基本不会走到这里:一般我们如果需要可以使用此方法作为初始化资源使用的目的!
                    //Preconditions.checkNotNull(clueLogic,"not support clueLogic is inject this class subject!");
                    if (Objects.isNull(clueLogic)) {
                        clueLogic = SpringUtils.getBean(ClueLogic.class);
                    }
                    customerImportVO = new CustomerImportVO();
                    // 此部分主要是为了减少不必要的内存空间的申请
                    tempDataList = Lists.newArrayListWithExpectedSize(batchSizeUnit);
                }
    //            syncLockController.lock();
            } catch (Exception e) {
                log.error("invoke the analysis the title head info data is failure!",e);
                throw new UnsupportedOperationException("invoke the analysis the title head info data is failure!",e);
            }
            log.info("【finished read the excel head data】");
        }
    
    数据处理
    invoke()方法

    一条一条数据解析 invoke()方法 ,方法里面是我业务逻辑,数据校验。invoke 就是每行具体的数据值

        /**
         * 调用操作处理控制机制
         * @param excelEntity
         * @param context
         */
        @Override
        public void invoke(ContactInfoExcelEntity excelEntity, AnalysisContext context) {
            log.info("----【start read the excel main data:{}】----",excelEntity);
            if(batchSizeUnit <= tempDataList.size()){
                CustomerImportVO customerImportVO = clueLogic.startCallTaskProxy(contactInfoImportParam,tempDataList);
                // 合并计算结果->更新为最新的结果
                this.customerImportVO.merge(customerImportVO);
                tempDataList.clear();
                tempDataList = Lists.newArrayListWithExpectedSize(batchSizeUnit);
            }else{
                tempDataList.add(excelEntity);
            }
            log.info("【finished read the excel main data】");
        }
    
    执行中断
    hasNextdoAfterAllAnalysed()方法
    /**
     * 是否拥有下一次执行
     * [@param](https://my.oschina.net/u/2303379) context
     * [@return](https://my.oschina.net/u/556800)
     */
    [@Override](https://my.oschina.net/u/1162528)
    public boolean hasNext(AnalysisContext context) {
        return causeByHeadFormatAbort?Boolean.FALSE:isSupportAbort? failureDataCount <= 0 :Boolean.TRUE;
    }
    
    数据完成
    doAfterAllAnalysed()方法

    所有数据解析完, doAfterAllAnalysed()方法,里面写的有保存数据方法。

        /**
         * 执行结束的回调机制
         * @param analysisContext
         */
        @Override
        public void doAfterAllAnalysed(AnalysisContext analysisContext) {
            log.info("【doAfterAllAnalysed the process】");
            try {
                CustomerImportVO customerImportVO = clueLogic.startCallTaskProxy(contactInfoImportParam,tempDataList);
                this.customerImportVO.merge(customerImportVO);
                finisheDataResult = Boolean.TRUE;
            }catch (Exception e){
                log.error("execute finially the flush data is failure!");
                //TODO 收尾的数据信息如何做到一致性和完成补偿!
                finisheDataResult =  Boolean.FALSE;
            } finally {
                tempDataList.clear();
    //            syncLockController.unlock();
            }
        }
    

    资料参考

    https://blog.csdn.net/weixin_39929602/article/details/112189135?spm=1001.2101.3001.6650.1&utm_medium=distribute.pc_relevant.none-task-blog-2defaultCTRLISTdefault-1.no_search_link&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2defaultCTRLISTdefault-1.no_search_link

    极限就是为了超越而存在的
  • 相关阅读:
    什么叫开漏输出
    PIC16F877A TIMER1计数操作
    [转载]【Alientek STM32 实验2】按键输入
    STM32学习笔记1 IO口学习
    时间“四象限”法
    CLANNAD AFTER STORY 片头曲 「铭刻时间的歌」
    正则表达式测试工具
    War3窗口限定小工具发布
    利用ffmpeg转换mp4文件
    一个类似FlashGet的c#开源下载工具
  • 原文地址:https://www.cnblogs.com/liboware/p/15707359.html
Copyright © 2020-2023  润新知