EasyExcel 工作原理

前言

以 post 方式在 controller 层接收 excel 文件,controller 方法的参数设置为: @RequestParam(“excelFile”) MultipartFile file。
注意问题:
当收到 excel 文件时,当前线程(假设为线程 A)会在临时路径下创建一个临时文件(即 excel 文件内容), MultipartFile file 就指向这个临时文件。
处理 excel 文件往往需要比较长的时间,用同步处理不太合适,往往使用异步方式,将 MultipartFile 传递给异步线程,当异步线程(假设为线程 B)启动时,如果线程 A 结束了,那么临时文件会被删除,此时异步线程 B 手里的 MultipartFile 就没用了,因为实际上的 excel 内容已经没有了,MultipartFile 仅仅只包含 excel 文件的一些属性信息,比如文件名,文件大小等等,通过 MultipartFile 读不出任何内容。
解决方法:
线程 A 传递文件给线程 B 时,改为传递 InputStream,而不是 MultipartFile。 即 file.getInputStream();

1. EasyExcel 类

EasyExcel 类什么都没有,只是简单继承了 EasyExcelFactory,因此,EasyExcelFactory 才是核心。

public class EasyExcel extends EasyExcelFactory {
    public EasyExcel() {
    }
}

2. EasyExcelFactory 类源码

2.1 EasyExcelFactory 中读 excel 的源码部分

从如下源码可以看出,EasyExcelFactory 使用了构造模式。 下列源码中,有非常多个 read 方法,最终使用的都是 ExcelReaderBuilder 类的构造模式来创建读 excel 的对象。
read 方法的参数无非就是:file(excel 文件),readListener(专门一行一行读取 excel 文件的工具),head(将 excel 按照头部映射为一个 class),pathName(excel 文件路径名),inputstream(excel 文件的输入流)。
关键在于看 ExcelReaderBuilder 究竟要构造怎么样的对象。

	public static ExcelReaderBuilder read() {
        return new ExcelReaderBuilder();
    }
    public static ExcelReaderBuilder read(File file) {
        return read((File)file, (Class)null, (ReadListener)null);
    }
    public static ExcelReaderBuilder read(File file, ReadListener readListener) {
        return read((File)file, (Class)null, readListener);
    }
    public static ExcelReaderBuilder read(File file, Class head, ReadListener readListener) {
        ExcelReaderBuilder excelReaderBuilder = new ExcelReaderBuilder();
        excelReaderBuilder.file(file);
        if (head != null) {
            excelReaderBuilder.head(head);
        }
        if (readListener != null) {
            excelReaderBuilder.registerReadListener(readListener);
        }
        return excelReaderBuilder;
    }
    public static ExcelReaderBuilder read(String pathName) {
        return read((String)pathName, (Class)null, (ReadListener)null);
    }
    public static ExcelReaderBuilder read(String pathName, ReadListener readListener) {
        return read((String)pathName, (Class)null, readListener);
    }
    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;
    }
    public static ExcelReaderBuilder read(InputStream inputStream) {
        return read((InputStream)inputStream, (Class)null, (ReadListener)null);
    }
    public static ExcelReaderBuilder read(InputStream inputStream, ReadListener readListener) {
        return read((InputStream)inputStream, (Class)null, readListener);
    }
    public static ExcelReaderBuilder read(InputStream inputStream, Class head, ReadListener readListener) {
        ExcelReaderBuilder excelReaderBuilder = new ExcelReaderBuilder();
        excelReaderBuilder.file(inputStream);
        if (head != null) {
            excelReaderBuilder.head(head);
        }
        if (readListener != null) {
            excelReaderBuilder.registerReadListener(readListener);
        }
        return excelReaderBuilder;
    }

2.2 ExcelReaderBuilder 构造的是 ReadWorkbook

ReadWorkbook 类记录的是读 excel 这个动作的所有属性,比如读取的是哪种 excel 文件(xls,还是 xlsx),哪个文件,是否忽略空行,等等。
具体有哪些属性,看看下面源码:

public class ReadWorkbook extends ReadBasicParameter {
    private ExcelTypeEnum excelType; //读取哪类excel,这是枚举,包含 .xls 和 .xlsx
    private InputStream inputStream; // 文件的输入流
    private File file; // 文件
    private Boolean mandatoryUseInputStream; // 是否强制使用文件输入流方式
    private Boolean autoCloseStream; // 是否自动关闭输入流
    private Object customObject; // 不清楚 
    private ReadCache readCache; // 缓存,当需要把 excel 的全部/部分读取到内存时,用缓存存储
    private Boolean ignoreEmptyRow; // 是否忽略空行
    private ReadCacheSelector readCacheSelector; // 不清楚
    private String password; // 打开excel文件的密码
    private String xlsxSAXParserFactoryName; 
    private Boolean useDefaultListener; // 是否使用默认的监听器
    private Set<CellExtraTypeEnum> extraReadSet; // 额外读取单元格的哪些信息
}
public class ReadBasicParameter extends BasicParameter {
    private Integer headRowNumber; // 表头为多少行
    private List<ReadListener> customReadListenerList = new ArrayList();
}
public class BasicParameter {
    private List<List<String>> head; // 不清楚怎么用
    private Class clazz; // excel 数据映射的类
    private List<Converter> customConverterList; 
    private Boolean autoTrim; // 自动去除首尾空格
    private Boolean use1904windowing; 
    private Locale locale;
}

传递过程挺复杂,没必要扣细节。
最终会调用 ReadListener 的几个方法,ReadListener 是接口,我们可以实现接口,并实现接口的几个方法,这样,就可以达到在该几个方法中处理 excel 数据的目的。

2.3 ReadListener 接口

public interface ReadListener<T> extends Listener {
    void onException(Exception var1, AnalysisContext var2) throws Exception;
    void invokeHead(Map<Integer, CellData> var1, AnalysisContext var2);
    void invoke(T var1, AnalysisContext var2);
    void extra(CellExtra var1, AnalysisContext var2);
    void doAfterAllAnalysed(AnalysisContext var1);
    boolean hasNext(AnalysisContext var1);
}
  1. invokeHead 方法:读取头部,那前几行是头部呢?ReadWorkbook 中 headRowNumber 属性决定,
    headRowNumber 等于几,那么 invokeHead 方法就执行几次,每次只读取一行表头,存储在参数 Map<Integer, CellData> var1 中,一般情况下我们自定义的 ReadListener 的 invokeHead 方法都是 invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context),Map 接口存储的就是一行表头,key 表示第几列,从 0 开始,value 表示表头的内容。
  2. invoke 方法:当 invokeHead 方法执行完后,就执行 invoke 方法,invoke 方法表示读取 excel 内容数据,没执行一次,就读取一行数据,参数 var1 就表示一行数据。
  3. extra 方法:excel 的单元格除了内容外,可能还有注释,超链接等等,此方法就用于读取单元的额外信息。
  4. doAfterAllAnalysed 方法:当读取完 excel 的数据后,就执行一次此方法,用于做结束工作。
  5. hasNext 方法:不清楚。

2.4 同步读取例子

从下列例子可以看出,如果 excel 的数据量不是很大,且只是简单地把数据读取出来,可以直接使用默认的同步监听器来读取即可,默认监听器也是 invoke 方法一次次执行,将 excel 数据添加到一个 list 中,当读取完毕后,将 list 返回。注意,当 excel 非常大时,list 会占用非常大的内存,容易导致溢出,因此,建议后端在收到 excel 文件时,虽然无法判断 excel 包含多少条数据,但是可以判断一下文件的大小,比如不能超过 5M,超过则不处理。
head 方法是用于表示接收 excel 数据的类是什么,建议类的每个变量都设置成 String ,再做类型转换,在类型转换时,最好使用 try catch 捕获转换失败异常,以便给出具体的失败反馈。

sheet 方法用于表示读取第几页,默认读取第一页。

excel 文件:
在这里插入图片描述

public class ExcelReadEntity {
//    @ExcelProperty(index = 0, value = "标识")
    private String id;
//    @ExcelProperty(index = 1, value = "名字")
    private String name;
    // 省略 getter  setter
}
	List<ExcelReadEntity> list = EasyExcel.read(file.getInputStream())
            .useDefaultListener(true)
            .ignoreEmptyRow(true)
            .autoCloseStream(true)
            .mandatoryUseInputStream(true)
            .excelType(ExcelTypeEnum.XLSX)
            .head(ExcelReadEntity.class)
            .sheet()
            .headRowNumber(1)
            .autoTrim(true)
            .doReadSync();
     System.out.println(list);

运行结果:
在这里插入图片描述
@ExcelProperty(index = 0, value = “标识”) 用以表示 excel 表头, index 表示匹配第几列,从 0 开始, value 表示匹配什么内容的表头。 index 和 value 二选一就可以,也可以同时使用,当同时使用时,index 与 value 是或的关系,只要有一个能匹配上就行。

2.5 异步读取例子

当 excel 文件的数据量非常大时,处理完需要较长时间,如果同步读取,那将等待很久,考虑使用异步读取方式。 建议使用 try catch 包裹,这样就能接收到来自监听器中 onException 方法抛出的异常了,可以根据异常的消息来判断异常的类型,便于给用户反馈异常的原因。

try{
	EasyExcel.read(file.getInputStream(), ExcelReadEntity.class, new ExcelReadListener())
         .excelType(ExcelTypeEnum.XLSX)
         .autoCloseStream(true)
         .autoTrim(true)
         .ignoreEmptyRow(true)
         .sheet()
         .headRowNumber(1)
         .doRead();
} catch (IOException e) {
     e.printStackTrace();
} catch (NotOfficeXmlFileException e){
     log.info("文件类型不正确");
} catch (ExcelAnalysisException e){
     log.info("文件解析出错");
} catch (Exception e){
     log.info("未知异常");
}

具体的解析工作都在监听器里面完成,一般会根据实际业务要求,判断 excel 数据的合法性,如果不合法,则抛出异常,或者记录下来。抛出异常的话,建议抛出 ExcelAnalysisException。

public class ExcelReadListener extends AnalysisEventListener<ExcelReadEntity> {
    @Override
    public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
        // 处理头部
    }
    @Override
    public void invoke(ExcelReadEntity excelReadEntity, AnalysisContext analysisContext) {
        // 处理每一条数据
    }
    @Override
    public void doAfterAllAnalysed(AnalysisContext analysisContext) {
        // 最后的工作
    }
    @Override
    public void onException(Exception exception, AnalysisContext context) throws Exception {
        // 当解析过程中,抛出异常,都会进入到这个方法中,用于异常的处理,并且,进一步向外层抛出异常,
    }
}

2.6 EasyExcelFactory 中写 excel 的源码部分

excel 的数据映射为 java 的类,列就对应成员变量,所以写 excel 就是将 List<映射对象> 写入一个文件中。
看看如下 EasyExcelFactory 中写 excel 的关键代码,也就是需要一个文件和映射类,表明将什么样类型的数据写入哪个文件中。

public static ExcelWriterBuilder write(File file, Class head)
public static ExcelWriterBuilder write(String pathName, Class head)
public static ExcelWriterBuilder write(OutputStream outputStream, Class head)

ExcelWriterBuilder 构造类又能设置哪些写属性呢?属性值是用 WriteWorkbook 类存储的。

public class WriteWorkbook extends WriteBasicParameter {
    private ExcelTypeEnum excelType; // 写入哪种类型文件,xls 或者 xlsx
    private File file; // excel 文件
    private OutputStream outputStream; // excel 输出流
    private InputStream templateInputStream; 
    private File templateFile;
    private Boolean autoCloseStream; // 自动关闭
    private Boolean mandatoryUseInputStream; // 是否强制使用输出流方式
    private String password; // excel 文件密码
    private Boolean inMemory; // excel 文件放在内存,还是放在磁盘
    private Boolean writeExcelOnException; 
}
public class WriteBasicParameter extends BasicParameter {
    private Integer relativeHeadRowIndex; // 头部最后一行的索引值,从 0 开始
    private Boolean needHead;  // 是否需要头部
    private List<WriteHandler> customWriteHandlerList = new ArrayList();
    private Boolean useDefaultStyle;  // 是否使用默认样式
    private Boolean automaticMergeHead;
    private Collection<Integer> excludeColumnIndexes;
    private Collection<String> excludeColumnFiledNames;
    private Collection<Integer> includeColumnIndexes;
    private Collection<String> includeColumnFiledNames;
}

用一个例子:

		try {
            File excelFile = File.createTempFile("hehe", ".xlsx");
            List<ExcelReadEntity> list = new ArrayList<>();
            ExcelReadEntity entity = new ExcelReadEntity();
            entity.setId(32535L);
            entity.setName("张三");
            list.add(entity);
            EasyExcel.write(excelFile, ExcelReadEntity.class)
                    .excelType(ExcelTypeEnum.XLSX)
                    .autoCloseStream(true)
                    .inMemory(true)
                    .password("hello")
                    .sheet()
                    .sheetName("第一页")
                    .doWrite(list);
            return excelFile;
        } catch (Exception e) {
            e.printStackTrace();
        }
  • 4
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
EasyExcel是一个基于Apache POI封装的Java类库,用于简化Excel文件的操作。其原理是通过解析Excel文件的XML格式,将数据转换为Java对象,或者将Java对象转换为Excel文件。下面是EasyExcel的主要原理: 1. Excel文件结构:Excel文件是由多个工作表(Sheet)组成的,每个工作表由多个行(Row)组成,每行又由多个单元格(Cell)组成。EasyExcel通过POI提供的API解析Excel文件的XML格式,获取工作表、行、单元格等元数据。 2. 数据取:EasyExcel使用POI的事件驱动模式进行数据取。在Excel文件时,EasyExcel会创建一个事件监听器(EventListener),通过监听器逐行Excel中的数据。当取到一行数据时,监听器会触发相应的事件,应用程序可以在事件中处理取到的数据。 3. 数据入:EasyExcel通过POI提供的API将Java对象转换为Excel文件。在Excel文件时,EasyExcel会将Java对象转换为对应的XML格式,并将XML数据入到Excel文件中。 4. 注解配置:EasyExcel提供了注解配置的方式来定义Java对象与Excel文件之间的映射关系。通过注解,可以指定Java对象的字段与Excel文件中的列之间的对应关系。这样,在Excel文件时,EasyExcel会自动将对应列的值赋给Java对象的字段;在Excel文件时,EasyExcel会根据注解配置将Java对象的字段值入到对应的列中。 总之,EasyExcel通过封装POI的API,简化了Excel文件的操作,提供了方便易用的方式来处理Excel数据。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值