Java操作Excel神器,easyExcel 源码分析

1,509 阅读4分钟

「这是我参与2022首次更文挑战的第6天,活动详情查看:2022首次更文挑战

easyExcel简介

项目有个需求就是需要上传Excel,然后读取数据,进行一系列的操作,很久之前就看到EasyExcel,但是一直没用过,今天试着用一下。

Java领域解析、操作Excel比较有名的框架有Apache poi、jxl等。但他们都存在一个严重的问题就是非常的耗内存。如果你的系统并发量不大的话可能还行,但是一旦并发上来后一定会OOM或者JVM频繁的full gc。

easyExcel是阿里巴巴开源的一个excel处理框架,以使用简单、节省内存著称。

64M内存1分钟内读取75M(46W行25列)的Excel(当然还有急速模式能更快,但是内存占用会在100M多一点)

官方说明:Alibaba Easy Excel - 简单、省内存的Java解析Excel工具 | 读Excel

类的概念

AnalysisContext 核心接口,也是整个操作期间的上下文。

ExcelWriter 写入Excel的类

ExcelReader 读取Excel的类

Cell 对应一行数据 sheet 对应单个表格

简单读取

因为我项目中涉及的就是Excel的读取,所以这边就简单说一下读取,也是来自官方的例子。

image.png

每行数据对应的数据结构:

@Data
public class DemoData {
    private String string;
    private Date date;
    private Double doubleData;
}

监听器,读取到的每一行数据都会进入到invoke回调

// 有个很重要的点 DemoDataListener 不能被spring管理,要每次读取excel都要new,然后里面用到spring可以构造方法传进去

public class DemoDataListener extends AnalysisEventListener<DemoData> {
    private static final Logger LOGGER = LoggerFactory.getLogger(DemoDataListener.class);
    /**
     * 每隔5条存储数据库,实际使用中可以3000条,然后清理list ,方便内存回收
     */
    private static final int BATCH_COUNT = 5;
    List<DemoData> list = new ArrayList<DemoData>();
    /**
     * 假设这个是一个DAO,当然有业务逻辑这个也可以是一个service。当然如果不用存储这个对象没用。
     */
    private DemoDAO demoDAO;
​
    public DemoDataListener() {
        // 这里是demo,所以随便new一个。实际使用如果到了spring,请使用下面的有参构造函数
        demoDAO = new DemoDAO();
    }
​
    /**
     * 如果使用了spring,请使用这个构造方法。每次创建Listener的时候需要把spring管理的类传进来
     *
     * @param demoDAO
     */
    public DemoDataListener(DemoDAO demoDAO) {
        this.demoDAO = demoDAO;
    }
​
    /**
     * 这个每一条数据解析都会来调用
     *
     * @param data
     *            one row value. Is is same as {@link AnalysisContext#readRowHolder()}
     * @param context
     */
    @Override
    public void invoke(DemoData data, AnalysisContext context) {
        LOGGER.info("解析到一条数据:{}", JSON.toJSONString(data));
        list.add(data);
        // 达到BATCH_COUNT了,需要去存储一次数据库,防止数据几万条数据在内存,容易OOM
        if (list.size() >= BATCH_COUNT) {
            saveData();
            // 存储完成清理 list
            list.clear();
        }
    }
​
    /**
     * 所有数据解析完成了 都会来调用
     *
     * @param context
     */
    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        // 这里也要保存数据,确保最后遗留的数据也存储到数据库
        saveData();
        LOGGER.info("所有数据解析完成!");
    }
​
    /**
     * 加上存储数据库
     */
    private void saveData() {
        LOGGER.info("{}条数据,开始存储数据库!", list.size());
        demoDAO.save(list);
        LOGGER.info("存储数据库成功!");
    }
}

读取数据发起点:

/**
 * 最简单的读
 * <p>1. 创建excel对应的实体对象 参照{@link DemoData}
 * <p>2. 由于默认一行行的读取excel,所以需要创建excel一行一行的回调监听器,参照{@link DemoDataListener}
 * <p>3. 直接读即可
 */
@Test
public void simpleRead() {
    // 有个很重要的点 DemoDataListener 不能被spring管理,要每次读取excel都要new,然后里面用到spring可以构造方法传进去
    // 写法1:
    String fileName = TestFileUtil.getPath() + "demo" + File.separator + "demo.xlsx";
    // 这里 需要指定读用哪个class去读,然后读取第一个sheet 文件流会自动关闭
    EasyExcel.read(fileName, DemoData.class, new DemoDataListener()).sheet().doRead();
​
    // 写法2:
    fileName = TestFileUtil.getPath() + "demo" + File.separator + "demo.xlsx";
    ExcelReader excelReader = EasyExcel.read(fileName, DemoData.class, new DemoDataListener()).build();
    ReadSheet readSheet = EasyExcel.readSheet(0).build();
    excelReader.read(readSheet);
    // 这里千万别忘记关闭,读的时候会创建临时文件,到时磁盘会崩的
    excelReader.finish();
}

原理解析

简单理解:在读取的过程中每次读取一行数据,调用Listener中的invoke函数,把数据给调用者进行解析,处理。

image.png

源码跟踪:

使用上面的demo,

第一步在读取的地方打上断点,

第二步在invoke的地方打上断点

image.png

看下堆栈的调用顺序,然后从下往上将每一个堆栈点一下看一下代码 的执行顺序,大概就知道这个执行流程。

image.png

下面摘抄一些核心代码

com.alibaba.excel.analysis.ExcelAnalyserImpl#analysis

前面的都是填充上下文数据,这里才开始真正的读取Excel

image.png

中间扫描Excel的过程不关注了,基本上就是对数据的读取,比较繁琐。

com.alibaba.excel.read.processor.DefaultAnalysisEventProcessor#endRow

这里是读取一行结束之后的处理

image.png

对数据的处理 com.alibaba.excel.read.processor.DefaultAnalysisEventProcessor#dealData

image.png

到这里已经能看到调用了设置的listener,在读到这里的时候我很好奇isData的设置,也就是表头是有多少行是怎么处理的。在我的代码中时没有做设置的。

image.png

默认的是第一行是表头

总结:

easyexcel 是真的简单