1. 程式人生 > 程式設計 >Excel解析工具easyexcel全面探索

Excel解析工具easyexcel全面探索

1. Excel解析工具easyexcel全面探索

1.1. 簡介

之前我們想到Excel解析一般是使用POI,但POI存在一個嚴重的問題,就是非常消耗記憶體。所以阿里人員對它進行了重寫從而誕生了easyexcel,它解決了過於消耗記憶體問題,也對它進行了封裝讓使用者使用更加便利

接下來我先一一介紹它所有的功能細節、如何使用及部分原始碼解析

1.2. Excel讀

1.2.1. 例子

    /**
     * 最簡單的讀
     * <p>1. 建立excel對應的實體物件 參照{@link DemoData}
     * <p>2. 由於預設非同步讀取excel,所以需要建立excel一行一行的回撥監聽器,參照{@link DemoDataListener}
     * <p>3. 直接讀即可
     */
    @Test
    public void simpleRead() {
        String fileName = TestFileUtil.getPath() + "demo" + File.separator + "demo.xlsx";
        // 這裡 需要指定讀用哪個class去讀,然後讀取第一個sheet 檔案流會自動關閉
        EasyExcel.read(fileName,DemoData.class,new DemoDataListener()).sheet().doRead();
    }複製程式碼

  • 官方說明也比較明確,使用簡單fileName路徑+檔名DemoData是Excel資料對應的實體類,DemoDataListener這看名字就是監聽器,用來監聽處理讀取到的每一條資料

1.2.2. 原始碼解析

1.2.2.1. 核心原始碼XlsxSaxAnalyser

  • 它核心的Excel解析我認為是這個類XlsxSaxAnalyser,在它的構造方法中做了很多事
    public XlsxSaxAnalyser(AnalysisContext analysisContext,InputStream decryptedStream) throws Exception {
         ...
         //從這開始將資料讀取成inputStream流,快取到了sheetMap
        XSSFReader xssfReader = new XSSFReader(pkg);
        analysisUse1904WindowDate(xssfReader,readWorkbookHolder);

        stylesTable = xssfReader.getStylesTable();
        sheetList = new ArrayList<ReadSheet>();
        sheetMap = new HashMap<Integer,InputStream>();
        XSSFReader.SheetIterator ite = (XSSFReader.SheetIterator)xssfReader.getSheetsData();
        int index = 0;
        if (!ite.hasNext()) {
            throw new ExcelAnalysisException("Can not find any sheet!");
        }
        while (ite.hasNext()) {
            InputStream inputStream = ite.next();
            sheetList.add(new ReadSheet(index,ite.getSheetName()));
            sheetMap.put(index,inputStream);
            index++;
        }
    }複製程式碼

1.2.2.2. doRead

  • 例子中真正開始做解析任務的是doRead方法,不斷進入此方法,會看到真正執行的最後方法就是XlsxSaxAnalyser類的execute方法;可以看到如下方法中parseXmlSource解析的就是sheetMap快取的真正資料
    @Override
    public void execute(List<ReadSheet> readSheetList,Boolean readAll) {
        for (ReadSheet readSheet : sheetList) {
            readSheet = SheetUtils.match(readSheet,readSheetList,readAll,analysisContext.readWorkbookHolder().getGlobalConfiguration());
            if (readSheet != null) {
                analysisContext.currentSheet(readSheet);
                parseXmlSource(sheetMap.get(readSheet.getSheetNo()),new XlsxRowHandler(analysisContext,stylesTable));
                // The last sheet is read
                analysisContext.readSheetHolder().notifyAfterAllAnalysed(analysisContext);
            }
        }
    }複製程式碼

1.2.2.3. 概述DemoDataListener實現

  • 對應我們使用者需要手寫的程式碼,我們的監聽器DemoDataListener中有兩個實現方法如下,invoke就對應了上述程式碼中的parseXmlSourcedoAfterAllAnalysed對應了上述方法中的notifyAfterAllAnalysed,分別表示了先解析每一條資料和當最後一頁讀取完畢通知所有監聽器
    @Override
    public void invoke(DemoData data,AnalysisContext context) {
        LOGGER.info("解析到一條資料:{}",JSON.toJSONString(data));
        list.add(data);
        if (list.size() >= BATCH_COUNT) {
            saveData();
            list.clear();
        }
    }

    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        saveData();
        LOGGER.info("所有資料解析完成!");
    }複製程式碼

1.2.2.4. parseXmlSource具體實現

  • 看標識重點的地方,這是最核心的解析地
    private void parseXmlSource(InputStream inputStream,ContentHandler handler) {
        InputSource inputSource = new InputSource(inputStream);
        try {
            SAXParserFactory saxFactory = SAXParserFactory.newInstance();
            saxFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl",true);
            saxFactory.setFeature("http://xml.org/sax/features/external-general-entities",false);
            saxFactory.setFeature("http://xml.org/sax/features/external-parameter-entities",false);
            SAXParser saxParser = saxFactory.newSAXParser();
            XMLReader xmlReader = saxParser.getXMLReader();
            xmlReader.setContentHandler(handler);
            //重點
            xmlReader.parse(inputSource);
            inputStream.close();
        } catch (ExcelAnalysisException e) {
            throw e;
        } catch (Exception e) {
            throw new ExcelAnalysisException(e);
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    throw new ExcelAnalysisException("Can not close 'inputStream'!");
                }
            }
        }
    }複製程式碼

  • 由於這層層深入非常多,我用一張截圖來表現它的呼叫形式

1.2.2.5. notifyAfterAllAnalysed具體實現

  • 具體看notifyAfterAllAnalysed的程式碼,我們實現的DemoDataListener監聽器繼承AnalysisEventListener,而AnalysisEventListener實現ReadListener介面
    @Override
    public void notifyAfterAllAnalysed(AnalysisContext analysisContext) {
        for (ReadListener readListener : readListenerList) {
            readListener.doAfterAllAnalysed(analysisContext);
        }
    }複製程式碼

1.3. Excel寫

1.3.1. 例子

  • 如下例子,使用還是簡單的,和讀比較類似
    /**
     * 最簡單的寫
     * <p>1. 建立excel對應的實體物件 參照{@link com.alibaba.easyexcel.test.demo.write.DemoData}
     * <p>2. 直接寫即可
     */
    @Test
    public void simpleWrite() {
        String fileName = TestFileUtil.getPath() + "write" + System.currentTimeMillis() + ".xlsx";
        // 這裡 需要指定寫用哪個class去讀,然後寫到第一個sheet,名字為模板 然後檔案流會自動關閉
        // 如果這裡想使用03 則 傳入excelType引數即可
        EasyExcel.write(fileName,DemoData.class).sheet("模板").doWrite(data());
    }
    
    private List<DemoData> data() {
        List<DemoData> list = new ArrayList<DemoData>();
        for (int i = 0; i < 10; i++) {
            DemoData data = new DemoData();
            data.setString("字串" + i);
            data.setDate(new Date());
            data.setDoubleData(0.56);
            list.add(data);
        }
        return list;
    }複製程式碼

1.3.2. 原始碼解析

1.3.2.1. doWrite

  • 和讀一樣doWrite才是實際做事的,這次我們從這個入口跟進
    public void doWrite(List data) {
        if (excelWriter == null) {
            throw new ExcelGenerateException("Must use 'EasyExcelFactory.write().sheet()' to call this method");
        }
        excelWriter.write(data,build());
        excelWriter.finish();
    }複製程式碼

1.3.2.2. write

  • 很明顯,write是核心,繼續進入ExcelWriter類,看名字addContent就是新增資料了,由excelBuilderExcel建造者來新增,這是ExcelBuilderImpl
    public ExcelWriter write(List data,WriteSheet writeSheet,WriteTable writeTable) {
        excelBuilder.addContent(data,writeSheet,writeTable);
        return this;
    }複製程式碼

1.3.2.3. addContent

  • 可以看到如下,顯示封裝和例項化一些資料,建立了ExcelWriteAddExecutor寫資料執行器,核心就是add方法了
    @Override
    public void addContent(List data,WriteTable writeTable) {
        try {
            if (data == null) {
                return;
            }
            context.currentSheet(writeSheet,WriteTypeEnum.ADD);
            context.currentTable(writeTable);
            if (excelWriteAddExecutor == null) {
                excelWriteAddExecutor = new ExcelWriteAddExecutor(context);
            }
            //核心
            excelWriteAddExecutor.add(data);
        } catch (RuntimeException e) {
            finish();
            throw e;
        } catch (Throwable e) {
            finish();
            throw new ExcelGenerateException(e);
        }
    }複製程式碼

1.3.2.4. add

  • 可以看到很明顯在遍歷資料addOneRowOfDataToExcel插入到Excel表了
    public void add(List data) {
        if (CollectionUtils.isEmpty(data)) {
            return;
        }
        WriteSheetHolder writeSheetHolder = writeContext.writeSheetHolder();
        int newRowIndex = writeSheetHolder.getNewRowIndexAndStartDoWrite();
        if (writeSheetHolder.isNew() && !writeSheetHolder.getExcelWriteHeadProperty().hasHead()) {
            newRowIndex += writeContext.currentWriteHolder().relativeHeadRowIndex();
        }
        // BeanMap is out of order,so use fieldList
        List<Field> fieldList = new ArrayList<Field>();
        for (int relativeRowIndex = 0; relativeRowIndex < data.size(); relativeRowIndex++) {
            int n = relativeRowIndex + newRowIndex;
            addOneRowOfDataToExcel(data.get(relativeRowIndex),n,relativeRowIndex,fieldList);
        }
    }複製程式碼

1.3.2.5. addOneRowOfDataToExcel

  • 這裡先是做建立Excel行的準備,包括行的一些屬性處理器需不需要處理,之後我們的例子是插入java物件,進入addJavaObjectToExcel方法
    private void addOneRowOfDataToExcel(Object oneRowData,int n,int relativeRowIndex,List<Field> fieldList) {
        if (oneRowData == null) {
            return;
        }
        WriteHandlerUtils.beforeRowCreate(writeContext,Boolean.FALSE);
        Row row = WorkBookUtil.createRow(writeContext.writeSheetHolder().getSheet(),n);
        WriteHandlerUtils.afterRowCreate(writeContext,row,Boolean.FALSE);
        if (oneRowData instanceof List) {
            addBasicTypeToExcel((List)oneRowData,relativeRowIndex);
        } else {
            addJavaObjectToExcel(oneRowData,fieldList);
        }
        WriteHandlerUtils.afterRowDispose(writeContext,Boolean.FALSE);
    }複製程式碼

1.3.2.6. addJavaObjectToExcel

  • ExcelWriteAddExecutor執行器類中執行addJavaObjectToExcel,在這裡進行了資料的解析,將資料解析成標題和內容,封裝成適合Excel的格式CellData,資料型別等,經過這步我們還沒看到檔案流的生成,那麼下一步了
    private void addJavaObjectToExcel(Object oneRowData,Row row,List<Field> fieldList) {
        WriteHolder currentWriteHolder = writeContext.currentWriteHolder();
        BeanMap beanMap = BeanMap.create(oneRowData);
        Set<String> beanMapHandledSet = new HashSet<String>();
        int cellIndex = 0;
        // If it's a class it needs to be cast by type
        if (HeadKindEnum.CLASS.equals(writeContext.currentWriteHolder().excelWriteHeadProperty().getHeadKind())) {
            Map<Integer,Head> headMap = writeContext.currentWriteHolder().excelWriteHeadProperty().getHeadMap();
            Map<Integer,ExcelContentProperty> contentPropertyMap =
                writeContext.currentWriteHolder().excelWriteHeadProperty().getContentPropertyMap();
            for (Map.Entry<Integer,ExcelContentProperty> entry : contentPropertyMap.entrySet()) {
                cellIndex = entry.getKey();
                ExcelContentProperty excelContentProperty = entry.getValue();
                String name = excelContentProperty.getField().getName();
                if (writeContext.currentWriteHolder().ignore(name,cellIndex)) {
                    continue;
                }
                if (!beanMap.containsKey(name)) {
                    continue;
                }
                Head head = headMap.get(cellIndex);
                WriteHandlerUtils.beforeCellCreate(writeContext,head,cellIndex,Boolean.FALSE);
                Cell cell = WorkBookUtil.createCell(row,cellIndex);
                WriteHandlerUtils.afterCellCreate(writeContext,cell,Boolean.FALSE);
                Object value = beanMap.get(name);
                CellData cellData = converterAndSet(currentWriteHolder,excelContentProperty.getField().getType(),value,excelContentProperty);
                WriteHandlerUtils.afterCellDispose(writeContext,cellData,Boolean.FALSE);
                beanMapHandledSet.add(name);
            }
        }
        // Finish
        if (beanMapHandledSet.size() == beanMap.size()) {
            return;
        }
        if (cellIndex != 0) {
            cellIndex++;
        }
        Map<String,Field> ignoreMap = writeContext.currentWriteHolder().excelWriteHeadProperty().getIgnoreMap();
        initFieldList(oneRowData.getClass(),fieldList);
        for (Field field : fieldList) {
            String filedName = field.getName();
            boolean uselessData = !beanMap.containsKey(filedName) || beanMapHandledSet.contains(filedName)
                || ignoreMap.containsKey(filedName) || writeContext.currentWriteHolder().ignore(filedName,cellIndex);
            if (uselessData) {
                continue;
            }
            Object value = beanMap.get(filedName);
            if (value == null) {
                continue;
            }
            WriteHandlerUtils.beforeCellCreate(writeContext,null,Boolean.FALSE);
            Cell cell = WorkBookUtil.createCell(row,cellIndex++);
            WriteHandlerUtils.afterCellCreate(writeContext,Boolean.FALSE);
            CellData cellData = converterAndSet(currentWriteHolder,value.getClass(),null);
            WriteHandlerUtils.afterCellDispose(writeContext,Boolean.FALSE);
        }
    }複製程式碼

1.3.2.7. finish

  • doWrite中之後還有一步finish
    public void finish() {
        excelBuilder.finish();
    }複製程式碼

  • 深入ExcelBuilderImpl
    @Override
    public void finish() {
        if (context != null) {
            context.finish();
        }
    }複製程式碼

  • WriteContextImpl寫內容實現類的finish方法中,我們可以看到writeWorkbookHolder.getWorkbook().write(writeWorkbookHolder.getOutputStream()); 這句是重點,將寫Excel持有容器中的內容流輸出;之後就是關閉流,刪除臨時檔案的過程
    @Override
    public void finish() {
        WriteHandlerUtils.afterWorkbookDispose(this);
        if (writeWorkbookHolder == null) {
            return;
        }
        Throwable throwable = null;

        boolean isOutputStreamEncrypt = false;
        try {
            isOutputStreamEncrypt = doOutputStreamEncrypt07();
        } catch (Throwable t) {
            throwable = t;
        }

        if (!isOutputStreamEncrypt) {
            try {
                // 重點
                writeWorkbookHolder.getWorkbook().write(writeWorkbookHolder.getOutputStream());
                writeWorkbookHolder.getWorkbook().close();
            } catch (Throwable t) {
                throwable = t;
            }
        }

        try {
            Workbook workbook = writeWorkbookHolder.getWorkbook();
            if (workbook instanceof SXSSFWorkbook) {
                ((SXSSFWorkbook)workbook).dispose();
            }
        } catch (Throwable t) {
            throwable = t;
        }

        try {
            if (writeWorkbookHolder.getAutoCloseStream() && writeWorkbookHolder.getOutputStream() != null) {
                writeWorkbookHolder.getOutputStream().close();
            }
        } catch (Throwable t) {
            throwable = t;
        }

        if (!isOutputStreamEncrypt) {
            try {
                doFileEncrypt07();
            } catch (Throwable t) {
                throwable = t;
            }
        }

        try {
            if (writeWorkbookHolder.getTempTemplateInputStream() != null) {
                writeWorkbookHolder.getTempTemplateInputStream().close();
            }
        } catch (Throwable t) {
            throwable = t;
        }

        clearEncrypt03();

        if (throwable != null) {
            throw new ExcelGenerateException("Can not close IO",throwable);
        }

        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Finished write.");
        }
    }複製程式碼

1.4. 檔案上傳

  • 它提供了一個接收InputStream的引數,之後和Excel讀沒多大區別
    /**
     * 檔案上傳
     * <p>
     * 1. 建立excel對應的實體物件 參照{@link UploadData}
     * <p>
     * 2. 由於預設非同步讀取excel,所以需要建立excel一行一行的回撥監聽器,參照{@link UploadDataListener}
     * <p>
     * 3. 直接讀即可
     */
    @PostMapping("upload")
    @ResponseBody
    public String upload(MultipartFile file) throws IOException {
        EasyExcel.read(file.getInputStream(),UploadData.class,new UploadDataListener()).sheet().doRead();
        return "success";
    }複製程式碼

1.5. 檔案下載

  • 寫入提供引數OutputStream,其它和檔案寫入差不多
    /**
     * 檔案下載
     * <p>
     * 1. 建立excel對應的實體物件 參照{@link DownloadData}
     * <p>
     * 2. 設定返回的 引數
     * <p>
     * 3. 直接寫,這裡注意,finish的時候會自動關閉OutputStream,當然你外面再關閉流問題不大
     */
    @GetMapping("download")
    public void download(HttpServletResponse response) throws IOException {
        // 這裡注意 有同學反應使用swagger 會導致各種問題,請直接用瀏覽器或者用postman
        response.setContentType("application/vnd.ms-excel");
        response.setCharacterEncoding("utf-8");
        // 這裡URLEncoder.encode可以防止中文亂碼 當然和easyexcel沒有關係
        String fileName = URLEncoder.encode("測試","UTF-8");
        response.setHeader("Content-disposition","attachment;filename=" + fileName + ".xlsx");
        EasyExcel.write(response.getOutputStream(),DownloadData.class).sheet("模板").doWrite(data());
    }複製程式碼

1.6. 讀取技巧

1.6.1. Excel讀取多頁

  • 以上都是最基礎的單頁讀寫,在我們呼叫sheet()方法時,實際上都是預設第1頁,那麼如何讀取多頁?
    /**
     * 讀多個或者全部sheet,這裡注意一個sheet不能讀取多次,多次讀取需要重新讀取檔案
     * <p>
     * 1. 建立excel對應的實體物件 參照{@link DemoData}
     * <p>
     * 2. 由於預設非同步讀取excel,所以需要建立excel一行一行的回撥監聽器,參照{@link DemoDataListener}
     * <p>
     * 3. 直接讀即可
     */
    @Test
    public void repeatedRead() {
        String fileName = TestFileUtil.getPath() + "demo" + File.separator + "demo.xlsx";
        // 讀取全部sheet
        // 這裡需要注意 DemoDataListener的doAfterAllAnalysed 會在每個sheet讀取完畢後呼叫一次。然後所有sheet都會往同一個DemoDataListener裡面寫
        EasyExcel.read(fileName,new DemoDataListener()).doReadAll();

        // 讀取部分sheet
        fileName = TestFileUtil.getPath() + "demo" + File.separator + "demo.xlsx";
        ExcelReader excelReader = EasyExcel.read(fileName).build();
        // 這裡為了簡單 所以註冊了 同樣的head 和Listener 自己使用功能必須不同的Listener
        ReadSheet readSheet1 =
            EasyExcel.readSheet(0).head(DemoData.class).registerReadListener(new DemoDataListener()).build();
        ReadSheet readSheet2 =
            EasyExcel.readSheet(1).head(DemoData.class).registerReadListener(new DemoDataListener()).build();
        // 這裡注意 一定要把sheet1 sheet2 一起傳進去,不然有個問題就是03版的excel 會讀取多次,浪費效能
        excelReader.read(readSheet1,readSheet2);
        // 這裡千萬別忘記關閉,讀的時候會建立臨時檔案,到時磁碟會崩的
        excelReader.finish();
    }複製程式碼

  • 可以看到doReadAll方法可以讀取所有sheet頁面
  • 若要讀取單獨的頁面,用第二種方式readSheet(index),index為頁面位置,從0開始計數

1.6.2. 自定義欄位轉換

  • 在讀取寫入的時候,我們可能會有這樣的需求:比如日期格式轉換,字串新增固定字首字尾等等,此時我們可以進行自定義編寫
@Data
public class ConverterData {
    /**
     * 我自定義 轉換器,不管資料庫傳過來什麼 。我給他加上“自定義:”
     */
    @ExcelProperty(converter = CustomStringStringConverter.class)
    private String string;
    /**
     * 這裡用string 去接日期才能格式化。我想接收年月日格式
     */
    @DateTimeFormat("yyyy年MM月dd日HH時mm分ss秒")
    private String date;
    /**
     * 我想接收百分比的數字
     */
    @NumberFormat("#.##%")
    private String doubleData;
}複製程式碼

  • 如上面的CustomStringStringConverter類為自定義轉換器,可以對字串進行一定修改,而日期數字的格式化,它已經有提供註解了DateTimeFormatNumberFormat
  • 轉換器如下,實現Converter介面後即可使用supportExcelTypeKey這是判斷單元格型別,convertToJavaData這是讀取轉換,convertToExcelData這是寫入轉換

import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.CellData;
import com.alibaba.excel.metadata.GlobalConfiguration;
import com.alibaba.excel.metadata.property.ExcelContentProperty;

public class CustomStringStringConverter implements Converter<String> {
    @Override
    public Class supportJavaTypeKey() {
        return String.class;
    }

    @Override
    public CellDataTypeEnum supportExcelTypeKey() {
        return CellDataTypeEnum.STRING;
    }

    /**
     * 這裡讀的時候會呼叫
     */
    @Override
    public String convertToJavaData(CellData cellData,ExcelContentProperty contentProperty,GlobalConfiguration globalConfiguration) {
        return "自定義:" + cellData.getStringValue();
    }

    /**
     * 這裡是寫的時候會呼叫 不用管
     */
    @Override
    public CellData convertToExcelData(String value,GlobalConfiguration globalConfiguration) {
        return new CellData(value);
    }

}複製程式碼

  • 這裡解析結果擷取部分如下,原資料是字串0 2020/1/1 1:01 1
解析到一條資料:{"date":"2020年01月01日01時01分01秒","doubleData":"100%","string":"自定義:字串0"}複製程式碼

1.6.3. 指定表頭行數

        EasyExcel.read(fileName,new DemoDataListener()).sheet()
            // 這裡可以設定1,因為頭就是一行。如果多行頭,可以設定其他值。不傳入也可以,因為預設會根據DemoData 來解析,他沒有指定頭,也就是預設1行
            .headRowNumber(1).doRead();複製程式碼

1.6.4. 讀取表頭資料

  • 只要在實現了AnalysisEventListener介面的監聽器中,重寫invokeHeadMap方法即可
    /**
     * 這裡會一行行的返回頭
     *
     * @param headMap
     * @param context
     */
    @Override
    public void invokeHeadMap(Map<Integer,String> headMap,AnalysisContext context) {
        LOGGER.info("解析到一條頭資料:{}",JSON.toJSONString(headMap));
    }複製程式碼

1.6.5. 轉換異常處理

  • 只要在實現了AnalysisEventListener介面的監聽器中,重寫onException方法即可
    @Override
    public void onException(Exception exception,AnalysisContext context) {
        LOGGER.error("解析失敗,但是繼續解析下一行:{}",exception.getMessage());
        if (exception instanceof ExcelDataConvertException) {
            ExcelDataConvertException excelDataConvertException = (ExcelDataConvertException)exception;
            LOGGER.error("第{}行,第{}列解析異常",excelDataConvertException.getRowIndex(),excelDataConvertException.getColumnIndex());
        }
    }複製程式碼

1.6.6. 讀取單元格引數和型別

  • 將類屬性用CellData封裝起來
@Data
public class CellDataReadDemoData {
    private CellData<String> string;
    // 這裡注意 雖然是日期 但是 型別 儲存的是number 因為excel 儲存的就是number
    private CellData<Date> date;
    private CellData<Double> doubleData;
    // 這裡並不一定能完美的獲取 有些公式是依賴性的 可能會讀不到 這個問題後續會修復
    private CellData<String> formulaValue;
}複製程式碼

  • 這樣讀取到的資料如下,會包含單元格資料型別
解析到一條資料:{"date":{"data":1577811661000,"dataFormat":22,"dataFormatString":"m/d/yy h:mm","formula":false,"numberValue":43831.0423726852,"type":"NUMBER"},"doubleData":{"data":1.0,"numberValue":1,"formulaValue":{"data":"字串01","formula":true,"formulaValue":"_xlfn.CONCAT(A2,C2)","stringValue":"字串01","type":"STRING"},"string":{"data":"字串0","dataFormat":0,"dataFormatString":"General","stringValue":"字串0","type":"STRING"}}複製程式碼

1.6.7. 同步返回

  • 不推薦使用,但如果特定情況一定要用,可以如下,主要為doReadSync方法,直接返回List
    /**
     * 同步的返回,不推薦使用,如果資料量大會把資料放到記憶體裡面
     */
    @Test
    public void synchronousRead() {
        String fileName = TestFileUtil.getPath() + "demo" + File.separator + "demo.xlsx";
        // 這裡 需要指定讀用哪個class去讀,然後讀取第一個sheet 同步讀取會自動finish
        List<Object> list = EasyExcel.read(fileName).head(DemoData.class).sheet().doReadSync();
        for (Object obj : list) {
            DemoData data = (DemoData)obj;
            LOGGER.info("讀取到資料:{}",JSON.toJSONString(data));
        }

        // 這裡 也可以不指定class,返回一個list,然後讀取第一個sheet 同步讀取會自動finish
        list = EasyExcel.read(fileName).sheet().doReadSync();
        for (Object obj : list) {
            // 返回每條資料的鍵值對 表示所在的列 和所在列的值
            Map<Integer,String> data = (Map<Integer,String>)obj;
            LOGGER.info("讀取到資料:{}",JSON.toJSONString(data));
        }
    }複製程式碼

1.6.8. 無物件的讀

  • 顧名思義,不建立實體物件來讀取Excel資料,那麼我們就用Map接收,但這種對日期不友好,對於簡單欄位的讀取可以使用
  • 其它都一樣,監聽器的繼承中泛型引數變為Map即可
public class NoModleDataListener extends AnalysisEventListener<Map<Integer,String>> {
    ...
}複製程式碼

  • 結果擷取如下
解析到一條資料:{0:"字串0",1:"2020-01-01 01:01:01",2:"1"}複製程式碼

1.7. 寫入技巧

1.7.1. 排除特定欄位和只寫入特定欄位

  • 使用excludeColumnFiledNames來排除特定欄位寫入,用includeColumnFiledNames表示只寫入特定欄位
    /**
     * 根據引數只匯出指定列
     * <p>
     * 1. 建立excel對應的實體物件 參照{@link DemoData}
     * <p>
     * 2. 根據自己或者排除自己需要的列
     * <p>
     * 3. 直接寫即可
     */
    @Test
    public void excludeOrIncludeWrite() {
        String fileName = TestFileUtil.getPath() + "excludeOrIncludeWrite" + System.currentTimeMillis() + ".xlsx";

        // 根據使用者傳入欄位 假設我們要忽略 date
        Set<String> excludeColumnFiledNames = new HashSet<String>();
        excludeColumnFiledNames.add("date");
        // 這裡 需要指定寫用哪個class去讀,然後寫到第一個sheet,名字為模板 然後檔案流會自動關閉
        EasyExcel.write(fileName,DemoData.class).excludeColumnFiledNames(excludeColumnFiledNames).sheet("模板")
            .doWrite(data());

        fileName = TestFileUtil.getPath() + "excludeOrIncludeWrite" + System.currentTimeMillis() + ".xlsx";
        // 根據使用者傳入欄位 假設我們只要匯出 date
        Set<String> includeColumnFiledNames = new HashSet<String>();
        includeColumnFiledNames.add("date");
        // 這裡 需要指定寫用哪個class去讀,然後寫到第一個sheet,名字為模板 然後檔案流會自動關閉
        EasyExcel.write(fileName,DemoData.class).includeColumnFiledNames(includeColumnFiledNames).sheet("模板")
            .doWrite(data());
    }複製程式碼

1.7.2. 指定寫入列

  • 寫入列的順序可以進行指定,在實體類註解上指定index,從小到大,從左到右排列
@Data
public class IndexData {
    @ExcelProperty(value = "字串標題",index = 0)
    private String string;
    @ExcelProperty(value = "日期標題",index = 1)
    private Date date;
    /**
     * 這裡設定3 會導致第二列空的
     */
    @ExcelProperty(value = "數字標題",index = 3)
    private Double doubleData;
}複製程式碼

1.7.3. 複雜頭寫入

  • 如下圖這種複雜頭

  • 我們可以通過修改實體類註解實現
@Data
public class ComplexHeadData {
    @ExcelProperty({"主標題","字串標題"})
    private String string;
    @ExcelProperty({"主標題","日期標題"})
    private Date date;
    @ExcelProperty({"主標題","數字標題"})
    private Double doubleData;
}複製程式碼

1.7.4. 重複多次寫入

  • 分為三種:1. 重複寫入同一個sheet;2. 同一個物件寫入不同sheet;3. 不同的物件寫入不同的sheet
    /**
     * 重複多次寫入
     * <p>
     * 1. 建立excel對應的實體物件 參照{@link ComplexHeadData}
     * <p>
     * 2. 使用{@link ExcelProperty}註解指定複雜的頭
     * <p>
     * 3. 直接呼叫二次寫入即可
     */
    @Test
    public void repeatedWrite() {
        // 方法1 如果寫到同一個sheet
        String fileName = TestFileUtil.getPath() + "repeatedWrite" + System.currentTimeMillis() + ".xlsx";
        // 這裡 需要指定寫用哪個class去讀
        ExcelWriter excelWriter = EasyExcel.write(fileName,DemoData.class).build();
        // 這裡注意 如果同一個sheet只要建立一次
        WriteSheet writeSheet = EasyExcel.writerSheet("模板").build();
        // 去呼叫寫入,這裡我呼叫了五次,實際使用時根據資料庫分頁的總的頁數來
        for (int i = 0; i < 5; i++) {
            // 分頁去資料庫查詢資料 這裡可以去資料庫查詢每一頁的資料
            List<DemoData> data = data();
            writeSheet.setSheetName("模板");
            excelWriter.write(data,writeSheet);
        }
        /// 千萬別忘記finish 會幫忙關閉流
        excelWriter.finish();

        // 方法2 如果寫到不同的sheet 同一個物件
        fileName = TestFileUtil.getPath() + "repeatedWrite" + System.currentTimeMillis() + ".xlsx";
        // 這裡 指定檔案
        excelWriter = EasyExcel.write(fileName,DemoData.class).build();
        // 去呼叫寫入,這裡我呼叫了五次,實際使用時根據資料庫分頁的總的頁數來。這裡最終會寫到5個sheet裡面
        for (int i = 0; i < 5; i++) {
            // 每次都要建立writeSheet 這裡注意必須指定sheetNo
            writeSheet = EasyExcel.writerSheet(i,"模板"+i).build();
            // 分頁去資料庫查詢資料 這裡可以去資料庫查詢每一頁的資料
            List<DemoData> data = data();
            excelWriter.write(data,writeSheet);
        }
        /// 千萬別忘記finish 會幫忙關閉流
        excelWriter.finish();

        // 方法3 如果寫到不同的sheet 不同的物件
        fileName = TestFileUtil.getPath() + "repeatedWrite" + System.currentTimeMillis() + ".xlsx";
        // 這裡 指定檔案
        excelWriter = EasyExcel.write(fileName).build();
        // 去呼叫寫入,這裡我呼叫了五次,實際使用時根據資料庫分頁的總的頁數來。這裡最終會寫到5個sheet裡面
        for (int i = 0; i < 5; i++) {
            // 每次都要建立writeSheet 這裡注意必須指定sheetNo。這裡注意DemoData.class 可以每次都變,我這裡為了方便 所以用的同一個class 實際上可以一直變
            writeSheet = EasyExcel.writerSheet(i,"模板"+i).head(DemoData.class).build();
            // 分頁去資料庫查詢資料 這裡可以去資料庫查詢每一頁的資料
            List<DemoData> data = data();
            excelWriter.write(data,writeSheet);
        }
        /// 千萬別忘記finish 會幫忙關閉流
        excelWriter.finish();
    }複製程式碼

1.7.5. 圖片匯出

  • 對圖片的匯出,可能會有這樣的需求,它提供了四種資料型別的匯出,還是很豐富的
    @Test
    public void imageWrite() throws Exception {
        String fileName = TestFileUtil.getPath() + "imageWrite" + System.currentTimeMillis() + ".xlsx";
        // 如果使用流 記得關閉
        InputStream inputStream = null;
        try {
            List<ImageData> list = new ArrayList<ImageData>();
            ImageData imageData = new ImageData();
            list.add(imageData);
            String imagePath = TestFileUtil.getPath() + "converter" + File.separator + "img.jpg";
            // 放入四種型別的圖片 實際使用只要選一種即可
            imageData.setByteArray(FileUtils.readFileToByteArray(new File(imagePath)));
            imageData.setFile(new File(imagePath));
            imageData.setString(imagePath);
            inputStream = FileUtils.openInputStream(new File(imagePath));
            imageData.setInputStream(inputStream);
            EasyExcel.write(fileName,ImageData.class).sheet().doWrite(list);
        } finally {
            if (inputStream != null) {
                inputStream.close();
            }
        }
    }複製程式碼

  • 圖片類為
@Data
@ContentRowHeight(100)
@ColumnWidth(100 / 8)
public class ImageData {
    private File file;
    private InputStream inputStream;
    /**
     * 如果string型別 必須指定轉換器,string預設轉換成string
     */
    @ExcelProperty(converter = StringImageConverter.class)
    private String string;
    private byte[] byteArray;
}複製程式碼

匯出結果:兩行四列,每列都對應一張圖片,四種匯出型別均可

image.png

  • 其中StringImageConverter自定義轉換器為
public class StringImageConverter implements Converter<String> {
    @Override
    public Class supportJavaTypeKey() {
        return String.class;
    }

    @Override
    public CellDataTypeEnum supportExcelTypeKey() {
        return CellDataTypeEnum.IMAGE;
    }

    @Override
    public String convertToJavaData(CellData cellData,GlobalConfiguration globalConfiguration) {
        throw new UnsupportedOperationException("Cannot convert images to string");
    }

    @Override
    public CellData convertToExcelData(String value,GlobalConfiguration globalConfiguration) throws IOException {
        return new CellData(FileUtils.readFileToByteArray(new File(value)));
    }

}複製程式碼

1.7.6. 欄位寬高設定

  • 設定實體類註解屬性即可
@Data
@ContentRowHeight(10)
@HeadRowHeight(20)
@ColumnWidth(25)
public class WidthAndHeightData {
    @ExcelProperty("字串標題")
    private String string;
    @ExcelProperty("日期標題")
    private Date date;
    /**
     * 寬度為50
     */
    @ColumnWidth(50)
    @ExcelProperty("數字標題")
    private Double doubleData;
}複製程式碼

1.7.7. 自定義樣式

  • 實現會比較複雜,需要做頭策略,內容策略,字型大小等
    @Test
    public void styleWrite() {
        String fileName = TestFileUtil.getPath() + "styleWrite" + System.currentTimeMillis() + ".xlsx";
        // 頭的策略
        WriteCellStyle headWriteCellStyle = new WriteCellStyle();
        // 背景設定為紅色
        headWriteCellStyle.setFillForegroundColor(IndexedColors.RED.getIndex());
        WriteFont headWriteFont = new WriteFont();
        headWriteFont.setFontHeightInPoints((short)20);
        headWriteCellStyle.setWriteFont(headWriteFont);
        // 內容的策略
        WriteCellStyle contentWriteCellStyle = new WriteCellStyle();
        // 這裡需要指定 FillPatternType 為FillPatternType.SOLID_FOREGROUND 不然無法顯示背景顏色.頭預設了 FillPatternType所以可以不指定
        contentWriteCellStyle.setFillPatternType(FillPatternType.SOLID_FOREGROUND);
        // 背景綠色
        contentWriteCellStyle.setFillForegroundColor(IndexedColors.GREEN.getIndex());
        WriteFont contentWriteFont = new WriteFont();
        // 字型大小
        contentWriteFont.setFontHeightInPoints((short)20);
        contentWriteCellStyle.setWriteFont(contentWriteFont);
        // 這個策略是 頭是頭的樣式 內容是內容的樣式 其他的策略可以自己實現
        HorizontalCellStyleStrategy horizontalCellStyleStrategy =
            new HorizontalCellStyleStrategy(headWriteCellStyle,contentWriteCellStyle);

        // 這裡 需要指定寫用哪個class去讀,然後寫到第一個sheet,名字為模板 然後檔案流會自動關閉
        EasyExcel.write(fileName,DemoData.class).registerWriteHandler(horizontalCellStyleStrategy).sheet("模板")
            .doWrite(data());
    }複製程式碼

  • 效果如下
    image.png

1.7.8. 單元格合併

    @Test
    public void mergeWrite() {
        String fileName = TestFileUtil.getPath() + "mergeWrite" + System.currentTimeMillis() + ".xlsx";
        // 每隔2行會合並。當然其他合併策略也可以自己寫
        LoopMergeStrategy loopMergeStrategy = new LoopMergeStrategy(2,0);
        // 這裡 需要指定寫用哪個class去讀,然後寫到第一個sheet,名字為模板 然後檔案流會自動關閉
        EasyExcel.write(fileName,DemoData.class).registerWriteHandler(loopMergeStrategy).sheet("模板").doWrite(data());
    }複製程式碼

  • 效果如下,第一列單元格資料,2,3兩行合併

image.png

1.7.9. 自動列寬

  • 根據作者描述,POI對中文的自動列寬適配不友好,easyexcel對數字也不能準確適配列寬,他提供的適配策略可以用,但不能精確適配,可以自己重寫
  • 想用就註冊處理器LongestMatchColumnWidthStyleStrategy
    @Test
    public void longestMatchColumnWidthWrite() {
        String fileName =
            TestFileUtil.getPath() + "longestMatchColumnWidthWrite" + System.currentTimeMillis() + ".xlsx";
        // 這裡 需要指定寫用哪個class去讀,然後寫到第一個sheet,名字為模板 然後檔案流會自動關閉
        EasyExcel.write(fileName,LongestMatchColumnWidthData.class)
            .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()).sheet("模板").doWrite(dataLong());
    }複製程式碼

1.7.10. 下拉,超連結

  • 下拉,超連結等功能需要自定義實現
    @Test
    public void customHandlerWrite() {
        String fileName = TestFileUtil.getPath() + "customHandlerWrite" + System.currentTimeMillis() + ".xlsx";
        // 這裡 需要指定寫用哪個class去讀,然後寫到第一個sheet,名字為模板 然後檔案流會自動關閉
        EasyExcel.write(fileName,DemoData.class).registerWriteHandler(new CustomSheetWriteHandler())
            .registerWriteHandler(new CustomCellWriteHandler()).sheet("模板").doWrite(data());
    }複製程式碼

  • 其中主要為處理器CustomCellWriteHandler類,其實現CellWriteHandler介面,我們在後處理方法afterCellDispose做處理
public class CustomCellWriteHandler implements CellWriteHandler {

    private static final Logger LOGGER = LoggerFactory.getLogger(CustomCellWriteHandler.class);

    @Override
    public void beforeCellCreate(WriteSheetHolder writeSheetHolder,WriteTableHolder writeTableHolder,Head head,Integer columnIndex,Integer relativeRowIndex,Boolean isHead) {

    }

    @Override
    public void afterCellCreate(WriteSheetHolder writeSheetHolder,Cell cell,Boolean isHead) {

    }

    @Override
    public void afterCellDispose(WriteSheetHolder writeSheetHolder,List<CellData> cellDataList,Boolean isHead) {
        // 這裡可以對cell進行任何操作
        LOGGER.info("第{}行,第{}列寫入完成。",cell.getRowIndex(),cell.getColumnIndex());
        if (isHead && cell.getColumnIndex() == 0) {
            CreationHelper createHelper = writeSheetHolder.getSheet().getWorkbook().getCreationHelper();
            Hyperlink hyperlink = createHelper.createHyperlink(HyperlinkType.URL);
            hyperlink.setAddress("https://github.com/alibaba/easyexcel");
            cell.setHyperlink(hyperlink);
        }
    }

}複製程式碼

1.7.11. 不建立物件的寫

  • 在設定write的時候不設定物件類,在head裡新增List>的物件頭
    @Test
    public void noModleWrite() {
        // 寫法1
        String fileName = TestFileUtil.getPath() + "noModleWrite" + System.currentTimeMillis() + ".xlsx";
        // 這裡 需要指定寫用哪個class去讀,然後寫到第一個sheet,名字為模板 然後檔案流會自動關閉
        EasyExcel.write(fileName).head(head()).sheet("模板").doWrite(dataList());
    }
    
    private List<List<String>> head() {
        List<List<String>> list = new ArrayList<List<String>>();
        List<String> head0 = new ArrayList<String>();
        head0.add("字串" + System.currentTimeMillis());
        List<String> head1 = new ArrayList<String>();
        head1.add("數字" + System.currentTimeMillis());
        List<String> head2 = new ArrayList<String>();
        head2.add("日期" + System.currentTimeMillis());
        list.add(head0);
        list.add(head1);
        list.add(head2);
        return list;
    }複製程式碼

1.8. 總結

  • 不知不覺列出了這麼多easyexcel的使用技巧和方式,這裡應該囊括了大部分我們工作中常用到的excel讀寫技巧,歡迎收藏查閱

easyexcel的github地址

歡迎訪問收藏作者知識點整理,沒註冊的請點選這裡