🏆【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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章