EasyExcel 专题 深度解析读流程核心源码

Posted Dream_it_possible!

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了EasyExcel 专题 深度解析读流程核心源码相关的知识,希望对你有一定的参考价值。

 

目录

一、官网地址和版本

二、读核心源码解读

1. 材料准备

2. EasyExcelFactory

3. 读excel---初始化

1) read()方法初始化ExcelReaderBuilder

2) .sheet()方法初始化ExcelReaderSheetBuilder

3) 根据excel类型选择对应的执行器executor的实现类XlsxAnalyser

4) 使用OPCPackage将Excel文件转换为Xml

5) 查看excel转换后的xml形式

4. 读excel---解析

1) 进入ExcelReaderSheetBuilder.doRead()方法

2) 转交给ExcelReader.read(ReadSheet... readSheet) 

3) 读取excel的入口 excelReaderExecutor.execute()

4) 如何解析xml?

5) 使用ModelBuildEventListener监听器映射成用户定义对象 

6) 执行收尾工作


       EasyExcel将excel文件转换为xml文件,以SAX的形式按行解析,占用内存代价极小,能轻松解析百万条记录。

一、官网地址和版本

地址: https://github.com/alibaba/easyexcel

版本: 3.1.0

克隆下来后,使用git tag命令查看所有版本

git tag

切换到指定版本 : v3.1.0
git checkout v3.1.0
编译源码:
mvn clean install -Dmaven.test.skip=true

 编译成功后,进入到easyexcel-test模块。

二、读核心源码解读

        首先我们看怎么将excel sheet页里的内容读到 java的DemoData对象里。

1. 材料准备

        用最新版的excel文件做演示,创建一个demo.xlsx文件,excel里有2个sheet页,分别为sheet1和sheet2, 内容相同,如下:

 对应的model对象DemoData, 对应excel表格里的3列。

@Getter
@Setter
@EqualsAndHashCode
public class DemoData 
    private String string;
    private Date date;
    private Double doubleData;

用DemoData按行接收数据,例如用PageListener监听器,每次通过consumer回调100行数据出来。

2. EasyExcelFactory

        EasyExcelFactory是EasyExcel的父类,它提供了开发者读和写的入口,我们可以借助EasyExcelFactory调用读、写操作。

        fileName = TestFileUtil.getPath() + "demo" + File.separator + "demo.xlsx";
        // 这里 需要指定读用哪个class去读,然后读取第一个sheet 文件流会自动关闭
        EasyExcel.read(fileName, DemoData.class, new DemoDataListener()).sheet().doRead();

        读取一个excel只需要三个参数: 带绝对路径的文件名,POJO的Class和一个监听器

        跟着EasyExcel.read()方法去解析读操作, 进入到EasyExcelFactory类,下面我将从初始化---->解析---> 收尾 这三个流程做解析。

3. 读excel---初始化

1) read()方法初始化ExcelReaderBuilder

        在read()方法里初始化一个ExcelReaderBuilder, 通过readBuilder设置文件路径,excel标题和注册监听器。

   public static ExcelReaderBuilder read(String pathName, Class head, ReadListener readListener) 
        ExcelReaderBuilder excelReaderBuilder = new ExcelReaderBuilder();
        // 设置文件路径
        excelReaderBuilder.file(pathName);
        //设置excel标题,也支持无标题
        if (head != null) 
            excelReaderBuilder.head(head);
        
        // 注册监听器
        if (readListener != null) 
            excelReaderBuilder.registerReadListener(readListener);
        
        return excelReaderBuilder;
    

        重要点: 根据ExcelReaderBuilder初始化一个ReadWorkBook类, ReadWorkBoook类包含了读取Excel的内容,用流的形式进行保存。

    private final ReadWorkbook readWorkbook;

    public ExcelReaderBuilder() 
        this.readWorkbook = new ReadWorkbook();
    

2) .sheet()方法初始化ExcelReaderSheetBuilder

        通过sheet()方法,通过build()方法初始化ExcelReaderSheetBuilder, 并设置表格名称、表格序号。

    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;
    

        主要点,在build()方法里初始化了excelReader和readSheet对象, 为后续读取工作做准备。

public ExcelReader build() 
    return new ExcelReader(readWorkbook);

        同时在ExcelReader的构造方法里初始化ExcelAnalyser的实现类ExcelAnalyserImpl, ExcelAnalyserImpl是分析excel的核心实现类


    /**
     * Analyser
     */
    private final ExcelAnalyser excelAnalyser;

    public ExcelReader(ReadWorkbook readWorkbook) 
        excelAnalyser = new ExcelAnalyserImpl(readWorkbook);
    

         然后用初始化好了的excelReader去初始化ExcelReaderSheetBuilder,同时初始readSheet对象后续会做为ExcelReader.read()方法的参数。

   private ExcelReader excelReader;
    /**
     * Sheet
     */
    private final ReadSheet readSheet;

    public ExcelReaderSheetBuilder() 
        this.readSheet = new ReadSheet();
    

    public ExcelReaderSheetBuilder(ExcelReader excelReader) 
        this.readSheet = new ReadSheet();
        this.excelReader = excelReader;
    

3) 根据excel类型选择对应的执行器executor的实现类XlsxAnalyser

        在ExcelAnalyserImpl的构造方法里实现了初始化ExcelReaderExecutor的操作,根据不同的文件类型,选择使用不同的Executor, 比如后缀名为xlsx类型的excel使用XlsxAnalyser解析器。

  public ExcelAnalyserImpl(ReadWorkbook readWorkbook) 
        try 
            // 根据excel的类型选择对应的executor
            choiceExcelExecutor(readWorkbook);
         catch (RuntimeException e) 
            finish();
            throw e;
         catch (Throwable e) 
            finish();
            throw new ExcelAnalysisException(e);
        
    

    private void choiceExcelExecutor(ReadWorkbook readWorkbook) throws Exception 
        ExcelTypeEnum excelType = ExcelTypeEnum.valueOf(readWorkbook);
        switch (excelType) 
            case XLS:
                POIFSFileSystem poifsFileSystem;
                if (readWorkbook.getFile() != null) 
                    poifsFileSystem = new POIFSFileSystem(readWorkbook.getFile());
                 else 
                    poifsFileSystem = new POIFSFileSystem(readWorkbook.getInputStream());
                
                // So in encrypted excel, it looks like XLS but it's actually XLSX
                if (poifsFileSystem.getRoot().hasEntry(Decryptor.DEFAULT_POIFS_ENTRY)) 
                    InputStream decryptedStream = null;
                    try 
                        decryptedStream = DocumentFactoryHelper
                            .getDecryptedStream(poifsFileSystem.getRoot().getFileSystem(), readWorkbook.getPassword());
                        XlsxReadContext xlsxReadContext = new DefaultXlsxReadContext(readWorkbook, ExcelTypeEnum.XLSX);
                        analysisContext = xlsxReadContext;
                        excelReadExecutor = new XlsxSaxAnalyser(xlsxReadContext, decryptedStream);
                        return;
                     finally 
                        IOUtils.closeQuietly(decryptedStream);
                        // as we processed the full stream already, we can close the filesystem here
                        // otherwise file handles are leaked
                        poifsFileSystem.close();
                    
                
                if (readWorkbook.getPassword() != null) 
                    Biff8EncryptionKey.setCurrentUserPassword(readWorkbook.getPassword());
                
                XlsReadContext xlsReadContext = new DefaultXlsReadContext(readWorkbook, ExcelTypeEnum.XLS);
                xlsReadContext.xlsReadWorkbookHolder().setPoifsFileSystem(poifsFileSystem);
                analysisContext = xlsReadContext;
                excelReadExecutor = new XlsSaxAnalyser(xlsReadContext);
                break;
            case XLSX:
                XlsxReadContext xlsxReadContext = new DefaultXlsxReadContext(readWorkbook, ExcelTypeEnum.XLSX);
                analysisContext = xlsxReadContext;
                // 同时通过openxml4j将excel文件转换为xml文件
                excelReadExecutor = new XlsxSaxAnalyser(xlsxReadContext, null);
                break;
            case CSV:
                CsvReadContext csvReadContext = new DefaultCsvReadContext(readWorkbook, ExcelTypeEnum.CSV);
                analysisContext = csvReadContext;
                excelReadExecutor = new CsvExcelReadExecutor(csvReadContext);
                break;
            default:
                break;
        
    

4) 使用OPCPackage将Excel文件转换为Xml

         接着上一步,进入到XlsxAnalyser的构造方法里,通过OPCPackage将excel文件转换为一个zip文件

 OPCPackage.open(xlsxReadWorkbookHolder.getFile());

里面包含了excel所有相关内容的xml形式的展示, OPCPackage的实现通过ZipPackage

5) 查看excel转换后的xml形式

        我们可以写一个小demo,使用opcPackage.save(path)方法将zip存入到磁盘上。

    public void testExcelTransferToXml() throws InvalidFormatException, IOException 
        // 使用OPCPackage打开一个压缩包
        String path = TestFileUtil.getPath() + "demo/";

        File file = new File(path + "demo.xlsx");
        OPCPackage opcPackage = OPCPackage.open(file);
        List<PackagePart> parts = opcPackage.getParts();
        File targetFile = new File(path + "result.zip");
        opcPackage.save(targetFile);
        System.out.println();
    

 打开zip文件后,长这样, sheet表单里的内容存放在worksheets目录下:


到此,一个Analyser的完整初始化流程就结束了,同时在executor的实现类里将EXCEL转换为XML,这些动作就是为了读而做准备。

4. 读excel---解析

1) 进入ExcelReaderSheetBuilder.doRead()方法

  public void doRead() 
        if (excelReader == null) 
            throw new ExcelGenerateException("Must use 'EasyExcelFactory.read().sheet()' to call this method");
        
        // build()方法初始化ExcelReaderBuilder,
        excelReader.read(build());
        excelReader.finish();
    

2) 转交给ExcelReader.read(ReadSheet... readSheet) 

        用初始化好的excelAnalyser去分析表格列表,一个ReadSheet就是一个sheet页。

 public ExcelReader read(List<ReadSheet> readSheetList) 
        excelAnalyser.analysis(readSheetList, Boolean.FALSE);
        return this;
    

3) 读取excel的入口 excelReaderExecutor.execute()

        ExcelAnalyserImpl里的anlysis方法里的execelReadExecutor.execute()方法是读取excel的入口, readAll参数默认为false。

    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 
                //执行excel读取的入口
                excelReadExecutor.execute();
             catch (ExcelAnalysisStopException e) 
                if (LOGGER.isDebugEnabled()) 
                    LOGGER.debug("Custom stop!");
                
            
         catch (RuntimeException e) 
            finish();
            throw e;
         catch (Throwable e) 
            finish();
            throw new ExcelAnalysisException(e);
        
    

        在analysis方法执行前,我们可以发现在AnalysisImpl的构造方法里根据Excel类型创建了不同类型的执行器choiceExcelExecutor(readWorkBook),其中CsvExcelReaderExecutor为解析CSV用的执行器、XlsSaxAnalyser为解析03版的excel的执行器(后缀为.xls的excel) 、XlsxSaxAnalyser 为解析07版excel的执行器。

        parseXmlSource方法继续xml文件里的内容。 

4) 如何解析xml?

        SAX提供了一个ContentHandler接口给开发者,我们可以实现ContentHandler接口来自定义内容处理器,重写ContentHandler里的startElement()方法即可,XlsxRowHandler为定义的实现类

parseXmlSource(sheetMap.get(readSheet.getSheetNo()), new XlsxRowHandler(xlsxReadContext));

        因此解析的时候会在XlsxRowHandler里的startElement方法里进行

        我们可以发现这里用到了策略模式,根据name获取到对应的XlsxTagHandler, 根据excel里的标签去选择对应的handler。 

        然后进入到RowTagHandler的startElement方法,按照xml的标签去解析。

5) 使用ModelBuildEventListener监听器映射成用户定义对象 

         当满足while条件时,进入到xlsxReadContext.analysisEventProcessor().endRow(xlsxReadContext);

         然后进入到dealData(analysisContext)方法,根据rowIndex当前行判断是否为数据,如果是数据,那么就执行监听器的invoke方法,如果是标题,那么就执行监听器的invokeHead()方法,analysisContext里的ReadHolder提供了一个默认的Listener-----ModelBuildEventListener

        ModelBuildEventListener的用处是将cellDataMap在builderUserModel转换为一个开发者指定的对象, 并用ReadRowHolder的currentRowAnalysisResult属性接收。

         返回用户对象后,会在for循环里执行下一个监听器,该监听器为我们指定的PageReaderListener。

         进入到PageReaderListener的invoke方法获取到用户对象。

    @Override
    public void invoke(T data, AnalysisContext context) 
        cachedDataList.add(data);
        if (cachedDataList.size() >= BATCH_COUNT) 
            consumer.accept(cachedDataList);
            cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
        
    

         然后每次读完BATCH_COUNT条数据,执行一次回调consumer.accept(cachedDataList)。

6) 执行收尾工作

        再次进入到XlsxSaxAnalyser的execute()方法, 解析完后,执行了收尾工作

xlsxReadContext.analysisEventProcessor().endSheet(xlsxReadContext);

        进入到PageReaderListener接口里的doAfterAllAnalysed, 如果cachedDataList不为空,继续回调。

@Override
public void doAfterAllAnalysed(AnalysisContext context) 
    if (CollectionUtils.isNotEmpty(cachedDataList)) 
        consumer.accept(cachedDataList);
    

         比如我定义的BACTH_COUNT=100, 每次满100条我才回调,我excel里只有10条记录,那么就不能在invoke方法里回调了,所以这里一定要进行收尾。

以上是关于EasyExcel 专题 深度解析读流程核心源码的主要内容,如果未能解决你的问题,请参考以下文章

EasyExcel 专题 深度解析读流程核心源码

大厂学苑 | 《RPC框架核心源码深度解析》专题上线!

读《Spring 源码深度解析》随记

高并发通过ThreadPoolExecutor类的源码深度解析线程池执行任务的核心流程

高并发通过ThreadPoolExecutor类的源码深度解析线程池执行任务的核心流程

好课资源共享:RPC框架核心源码深度解析