EasyExcel读文件详解和源码分析

读取文件导入的话,我们经常看到下面这些方法。

//同步的返回,不推荐使用
EasyExcel.read(file).sheet(sheetNo).head(Class.class).headRowNumber(headRowNum)..doReadSync(); 

//异步的,通过监听器处理读到的数据。
EasyExcel.read(file).sheet(sheetNo).head(Class.class).headRowNumber(headRowNum).registerReadListener(监听器)doRead(); 

首先建议大家可以看一下 EasyExcel为我们提供的 EasyExcelFactory工厂类相关的源代码,看懂源代码的话,对我们自己编写适合自己项目的方法很用用处。

引入依赖:

        
        <dependency>
            <groupId>com.alibabagroupId>
            <artifactId>easyexcelartifactId>
            <version>3.1.1version>
        dependency>

一、EasyExcel读取文件源码

1、EasyExcelFactory工厂类

方法:EasyExcel.read(file)

查看 read()方法:

EasyExcel读文件详解和源码分析_第1张图片

EasyExcel类调用 read方法,实际调用的是 EasyExcelFactory类的方法。

查看 EasyExcelFactory类方法:

EasyExcel读文件详解和源码分析_第2张图片

EasyExcelFactory工厂类定义了许多读和写的重载方法。主要看读方法,分为两类:

  • 读取Excel文件,返回 ExcelWriterBuilder类
  • 读取Excel文件中的sheet,返回 ExcelReaderSheetBuilder类

从这里我们可以看出,EasyExcelFactory工厂类创建了 XxxBuilder类,并返回了 XxxBuilder类,那是不是我们也可以直接使用 XxxBuilder类操作读文件。答案是肯定的。

2、ExcelWriterBuilder类

查看 ExcelReaderBuilder类:

EasyExcel读文件详解和源码分析_第3张图片

ExcelReaderBuilder类实例化时,创建了 ReadWorkbook对象。

查看 ExcelReaderBuilder类方法:

EasyExcel读文件详解和源码分析_第4张图片

ExcelReaderBuilder类主要处理 Excel文件和相关文件属性信息,比如设置 字符编码、文件加密的密码,忽略处理哪行数据等。

3、ExcelReaderSheetBuilder类

查看 ExcelReaderSheetBuilder类:

EasyExcel读文件详解和源码分析_第5张图片

ExcelReaderSheetBuilder类实例化时,创建了 ReadSheet对象和 ExcelReader对象。

查看 ExcelReaderSheetBuilder类方法:

EasyExcel读文件详解和源码分析_第6张图片

ExcelReaderSheetBuilder类主要处理 Excel文件中的每一个 sheet信息,比如设置要读取的sheet名,索引。

调用 doRead()方法其实底层通过 ExcelReader对象读取每一个 sheet信息。

4、ExcelReader类

查看 ExcelReader类:

EasyExcel读文件详解和源码分析_第7张图片

查看 ExcelReader类方法:

EasyExcel读文件详解和源码分析_第8张图片
这里主要看一下 read方法。

EasyExcel读文件详解和源码分析_第9张图片

可以看出 ExcelReader类包含了 ReadWorkbook对象和 ReadSheet对象。从而处理 Excel文件中的每一个 sheet信息。

思考:

(1)ExcelReader类是如何初始化的?

在 ExcelReaderBuilder类调用 sheet()方法时,初始化了 ExcelReader对象。

EasyExcel读文件详解和源码分析_第10张图片

在 ExcelReaderSheetBuilder类调用 doRead()方法时,底层就通过 ExcelReader对象遍历读取每一个 sheet信息。

EasyExcel读文件详解和源码分析_第11张图片

(2)ReadWorkbook对象和 ReadSheet对象是如何赋值给 ExcelReader类的?

在各自的XxxBuilder类中通过构造方法初始化时,创建的,然后分别在调用 sheet()方法和 doRead()方法各自的 build()方法中完成赋值的。

有了上面的知识,接下来通过 EasyExcel操作读 Excel文件就比较简单了。

二、读一个sheet

一般情况,我们创建一个对象来和 Excel文件sheet的列名建立映射关系。

指定列的下标或者列名:

  • 在字段上添加 @ExcelProperty注解,不建议 index 和 name 同时用,要么一个对象只用index,要么一个对象只用name去匹配。

自定义格式转换:

  • 可以自定义格式转换器,也可以使用自带的日期、数字格式转换。

下面我们创建一个Excel文件。

EasyExcel读文件详解和源码分析_第12张图片

根据 Sheet信息创建一个映射类:

@Getter
@Setter
@EqualsAndHashCode
public class DemoData {

    @ExcelProperty("标题")
    private String string;

    @ExcelProperty("日期")
    private Date date;

    @ExcelProperty("浮点数据")
    private Double doubleData;

}

1、同步读

同步读并返回数据,不推荐使用。

    public static void main(String[] args) {
        syncRead();
    }

    private static void syncRead() {
        String fileName = "D:\\TempFiles\\表格.xlsx";
        List<DemoData> demoDataList = EasyExcel.read(fileName)
                .sheet()
                .head(DemoData.class)
                .headRowNumber(1)
                .doReadSync(); //同步读
        log.info("同步解析到所有数据为:{}", JSON.toJSONString(demoDataList));
    }

2、异步读

注册一个自带的匿名监听器。

    public static void asyncRead() {
        String fileName = "D:\\TempFiles\\表格.xlsx";
        EasyExcel.read(fileName)// 读取Excel文件
                .sheet(0) // 读取哪个sheet,索引从0开始
                .head(DemoData.class) // 设置映射对象
                .headRowNumber(1) // 设置1,因为头值占了一行。如果多行头,就设置几行。索引从1开始
                .registerReadListener(new AnalysisEventListener<DemoData>() { //注册读的监听器
                    /**
                     * 每解析一行excel数据,就会被调用一次
                     * @param demoData
                     * @param analysisContext
                     */
                    @Override
                    public void invoke(DemoData demoData, AnalysisContext analysisContext) {
                        log.info("解析到一条数据为:{}", JSON.toJSONString(demoData));
                    }

                    /**
                     * 全部解析完被调用
                     * @param analysisContext
                     */
                    @Override
                    public void doAfterAllAnalysed(AnalysisContext analysisContext) {
                        log.info("全部解析完成");
                    }
                })
                .doRead();
    }

EasyExcel读文件详解和源码分析_第13张图片

三、读多个sheet

监听器可以理解为是对读取的数据进行校验(空校验,类型校验)和处理的逻辑部分,用于异步读取。

在上面异步读中的代码,使用了一个参数就是 AnalysisEventListener excelListener的监听器。

AnalysisEventListener类实现了 ReadListener接口,ReadListener中有下面几个方法:

public interface ReadListener<T> extends Listener {
 
    // 在转换异常获取其他异常下会调用本接口。
    default void onException(Exception exception, AnalysisContext context) throws Exception {
        throw exception;
    }
 
    //读取表头数据存在headMap中
    default void invokeHead(Map<Integer, ReadCellData<?>> headMap, AnalysisContext context) {}
 
    //读取一行一行数据到data
    void invoke(T data, AnalysisContext context);
 
    void extra(CellExtra var1, AnalysisContext context);
 
    //在完成所有数据解析后进行的操作。AOP思想。
    void doAfterAllAnalysed(AnalysisContext context);
 
    default boolean hasNext(AnalysisContext context) {
        return true;
    }
}

我们可以根据进行自己需求的扩展 AnalysisEventListener这个类或者ReadListener接口,对那几个方法进行扩展。

在上面的Excel文件中我们再创建一个Sheet。

EasyExcel读文件详解和源码分析_第14张图片

对应再创建一个映射类:

@Getter
@Setter
@EqualsAndHashCode
public class DemoData2 {

    @ExcelProperty("标题")
    private String string;

    @ExcelProperty("日期")
    private Date date;

    @ExcelProperty("浮点数据")
    private Double doubleData;

    @ExcelProperty("整数")
    private Integer integerData;

    /**
     * Java String类型会丢失精度,建议定义为 BigDecimal|Double类型
     */
    @ExcelProperty("经度")
    private Double longitude;

    @ExcelProperty("纬度")
    private Double latitude;

}

1、使用匿名监听器

    public static void manySheetRead() {
        String fileName = "D:\\TempFiles\\表格.xlsx";
        ExcelReader excelReader = EasyExcel.read(fileName).build();

        ReadSheet readSheet1 = EasyExcel.readSheet(0).head(DemoData.class).headRowNumber(1)
                .registerReadListener(new AnalysisEventListener<DemoData>() { //注册读的监听器
                    /**
                     * 每解析一行excel数据,就会被调用一次
                     * @param demoData
                     * @param analysisContext
                     */
                    @Override
                    public void invoke(DemoData demoData, AnalysisContext analysisContext) {
                        log.info("readSheet1 解析到一条数据为:{}", JSON.toJSONString(demoData));
                    }

                    /**
                     * 全部解析完被调用
                     * @param analysisContext
                     */
                    @Override
                    public void doAfterAllAnalysed(AnalysisContext analysisContext) {
                        log.info("readSheet1 全部解析完成");
                    }
                }).build();

        ReadSheet readSheet2 = EasyExcel.readSheet(1).head(DemoData2.class)
                .headRowNumber(2) // 注意Sheet2表头占了两行
                .registerReadListener(new AnalysisEventListener<DemoData2>() { //注册读的监听器
                    /**
                     * 每解析一行excel数据,就会被调用一次
                     * @param demoData2
                     * @param analysisContext
                     */
                    @Override
                    public void invoke(DemoData2 demoData2, AnalysisContext analysisContext) {
                        log.info("readSheet2 解析到一条数据为:{}", JSON.toJSONString(demoData2));
                    }

                    /**
                     * 全部解析完被调用
                     * @param analysisContext
                     */
                    @Override
                    public void doAfterAllAnalysed(AnalysisContext analysisContext) {
                        log.info("readSheet2 全部解析完成");
                    }
                }).build();

        excelReader.read(readSheet1, readSheet2);
    }

EasyExcel读文件详解和源码分析_第15张图片

2、自定义读监听器

2.1 自定义读监听器

自定义一个通用的读监听器,继承 AnalysisEventListener类。

@Slf4j
public class CustomEasyExcelReadListener<T> extends AnalysisEventListener<T> {
    // 保存读取的对象
    private final List<T> rows = new ArrayList<>();

    // Sheet对应的名字
    private String sheetName = "";

    // 获取对应类
    private Class headClazz;

    // 此集合用来存储错误信息
    private final List<String> errorMessage = new ArrayList<>();

    public CustomEasyExcelReadListener(Class headClazz) {
        this.headClazz = headClazz;
    }


    /**
     * 通过Class获取类字段信息
     *
     * @param headClazz
     * @return
     * @throws NoSuchFieldException
     */
    public Map<Integer, String> getIndexNameMap(Class headClazz) throws NoSuchFieldException {
        Map<Integer, String> result = new HashMap<>();
        Field field;
        Field[] fields = headClazz.getDeclaredFields();     //获取类中所有的属性
        for (int i = 0; i < fields.length; i++) {
            field = headClazz.getDeclaredField(fields[i].getName());
            //log.info(String.valueOf(field));
            field.setAccessible(true);
            ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class);//获取根据注解的方式获取ExcelProperty修饰的字段
            if (excelProperty != null) {
                int index = excelProperty.index();         //索引值
                String[] values = excelProperty.value();   //字段值
                StringBuilder value = new StringBuilder();
                for (String v : values) {
                    value.append(v);
                }
                result.put(index, value.toString());
            }
        }
        return result;
    }

    /**
     * 读取表头数据存在headMap中。如果你校验表头格式时可以使用。
     *
     * @param headMap
     * @param context
     */
    @Override
    public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
        log.info("解析到一条表头数据:{}", JSON.toJSONString(headMap));
        Map<Integer, String> head = new HashMap<>();
        try {
            //通过Class获取到使用@ExcelProperty注解配置的字段
            head = getIndexNameMap(headClazz);
            log.info(String.valueOf(head));
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
        //解析到的excel表头和实体配置的进行比对
        Set<Integer> keySet = head.keySet();
        for (Integer key : keySet) {
            if (StringUtils.isEmpty(headMap.get(key))) {
                errorMessage.add("您上传的文件第" + (key + 1) + "列表头为空,请按照模板检查后重新上传");
            }
            if (!headMap.get(key).equals(head.get(key))) {
                errorMessage.add("您上传的文件第" + (key + 1) + "列表头与模板表头不一致,请检查后重新上传");
            }
        }
    }

    /**
     * 读取一行一行数据到object
     *
     * @param object
     * @param context
     */
    @Override
    public void invoke(T object, AnalysisContext context) {
        // 实际数据量比较大时,rows里的数据可以存到一定量之后进行批量处理(比如存到数据库),
        // 然后清空列表,以防止内存占用过多造成OOM
        rows.add(object);
    }

    /**
     * 在完成数据解析后进行的操作。AOP思想。
     *
     * @param context
     */
    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        // 当前sheet的名称 编码获取类似
        sheetName = context.readSheetHolder().getSheetName();
        log.info("sheetName = {} -> 所有数据解析完成, read {} rows", sheetName, rows.size());
    }

    /**
     * 在转换异常 获取其他异常下会调用本接口。抛出异常则停止读取。如果这里不抛出异常则 继续读取下一行。
     *
     * @param exception 抛出异常
     * @param context   解析内容
     */
    @Override
    public void onException(Exception exception, AnalysisContext context) {
        log.error("解析失败,但是继续解析下一行:{}", exception.getMessage());
        if (exception instanceof ExcelDataConvertException) {
            ExcelDataConvertException excelDataConvertException = (ExcelDataConvertException) exception;
            errorMessage.add("第" + excelDataConvertException.getRowIndex() + "行,第" + (excelDataConvertException.getColumnIndex() + 1) +
                    "列数据类型解析异常,数据为:" + excelDataConvertException.getCellData());
            log.error("第{}行,第{}列数据类型解析异常,数据为:{}", excelDataConvertException.getRowIndex(), excelDataConvertException.getColumnIndex() + 1,
                    excelDataConvertException.getCellData());
        }
    }

    public List<T> getRows() {
        return rows;
    }

    public Class getHeadClazz() {
        return headClazz;
    }

    public List<String> getErrorMessage() {
        return errorMessage;
    }

    public String getSheetName() {
        return sheetName;
    }
}

2.2 实例测试

    /**
     * 自定义 ReadListener监听器测试方法
     */
    private static void manySheetReadWthCustomReadListener() {
        String fileName = "D:\\TempFiles\\表格.xlsx";
        ExcelReader excelReader = EasyExcel.read(fileName).build();

        CustomEasyExcelReadListener mySheet1Listener = new CustomEasyExcelReadListener(DemoData.class);
        CustomEasyExcelReadListener mySheet2Listener = new CustomEasyExcelReadListener(DemoData2.class);

        List<ReadSheet> readSheetList = new ArrayList<>();
        ReadSheet readSheet1 = EasyExcel.readSheet(0).head(DemoData.class).headRowNumber(1)
                .registerReadListener(mySheet1Listener).build();

        ReadSheet readSheet2 = EasyExcel.readSheet(1).head(DemoData2.class)
                .headRowNumber(2) // 注意Sheet2表头占了两行
                .registerReadListener(mySheet2Listener).build();

        readSheetList.add(readSheet1);
        readSheetList.add(readSheet2);
        excelReader.read(readSheetList);

        System.out.println("============Sheet1 解析解析完成,数据如下================");
        //获取Sheet1监听器读到的数据,拿到的数据大家可以根据需求进行数据库操作
        List rows = mySheet1Listener.getRows();
        for (Object row : rows) {
            log.info("Sheet1 解析到一条数据为:{}", JSON.toJSONString(row));
        }
        //获取解决出的错误信息
        List<String> errorMessage = mySheet1Listener.getErrorMessage();
        log.info("Sheet1 解析错误信息为:{}", JSON.toJSONString(errorMessage));


        System.out.println("============Sheet2 解析解析完成,数据如下================");
        //获取Sheet2监听器读到的数据,拿到的数据大家可以根据需求进行数据库操作
        List<DemoData2> demoData2List = mySheet2Listener.getRows();
        for (DemoData2 demoData2 : demoData2List) {
            log.info("Sheet2 解析到一条数据为:{}", JSON.toJSONString(demoData2));
        }
        //获取解决出的错误信息
        List<String> errorMessage2 = mySheet2Listener.getErrorMessage();
        log.info("Sheet2 解析错误信息为:{}", JSON.toJSONString(errorMessage2));
    }

EasyExcel读文件详解和源码分析_第16张图片

通用的自定义读监听器中 invokeHeadMap方法校验有点不合适,大家可以创建针对性的读监听器,分别处理。一般 invokeHeadMap方法根据需要使用。

更多操作查看官方文档:https://easyexcel.opensource.alibaba.com/

– 求知若饥,虚心若愚。

你可能感兴趣的:(Common,EasyExcel读文件详解,EasyExcel读源码分析)