【EasyExcel】入门学习及快速上手实践复盘

官方文档:https://easyexcel.opensource.alibaba.com/docs/current/

从Excel读入数据

最简单的读在这里不再赘述,参考文章最上方的官方文档(全中文的,里面已经写得很通俗易懂了)。这里只大致讲一下主要步骤:
1.定义接收数据的对象结构;
2.定义处理数据的监听器,其中包含每批数据解析完的清理机制和储存机制。拿官方文档的demo来举例:

  	/**
     * 每隔5条存储数据库,实际使用中可以100条,然后清理list ,方便内存回收
     */
    private static final int BATCH_COUNT = 100;
 	/**
     * 缓存的数据
     */
    private List<DemoData> cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
    
 	@Override
    public void invoke(DemoData data, AnalysisContext context) {
        log.info("解析到一条数据:{}", JSON.toJSONString(data));
        cachedDataList.add(data);
        // 达到BATCH_COUNT了,需要去存储一次数据库,防止数据几万条数据在内存,容易OOM
        if (cachedDataList.size() >= BATCH_COUNT) {
            saveData();
            // 存储完成清理 list
            cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
        }
    }

监听器里定义了BATCH_COUNT,每行解析出来的数据都会放入cachedDataList 中,cachedDataList 达到BATCH_COUNT会先存储这部分数据,然后将cachedDataList清掉。这样保证读是一批一批落库的,不会一下读大量数据OOM。

动态读

编码过程中遇到的实际情况是用户输入的excel是动态表头,我们无法使用特定的对象去接收数据,只能用最基础的Map去接收。参考官方文档中不创建对象的读。

private List<Map<Integer, String>> cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);

用来接收数据的结构是List>,其中Map>是每一行的数据,map的key是列序号,从0开始,value是此行此列单元格的值。

只解析部分数据

解决方案1:
参考了https://blog.csdn.net/u012751272/article/details/126298071的解决方案,感谢大佬。
思路就是解析完需要的数据后用抛出异常来中断后续解析流程,外层捕获异常即可,不影响解析功能,且能大幅提高性能。比如需求只需解析出20行示例数据,程序只需解析完这20行即可返回,无需等待整个表所有数据解析完。

解决方案2:
如果需求要求的是解析示例数据同时返回用户输入的excel的总行数,那就不能采用抛出异常中断流程的方法。代码(已脱敏)如下:

public class ParsePartExcelListener extends AnalysisEventListener<Map<Integer, String>> {

    private List<Map<Integer, String>> dataList = new ArrayList<>();
    private List<Map<Integer, String>> headList = new ArrayList<>();
    //需要解析的行数
    private Integer limitSize;
    //总行数
    private Integer rowCount = 0;

    private int BATCH_SIZE = 100;


    public ParsePartExcelListener(int limitSize) {
        this.limitSize = limitSize;
    }

    @Override
    public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
        headList.add(headMap);
    }

    @Override
    public void invoke(Map<Integer, String> data, AnalysisContext analysisContext) {
        if (!(Objects.nonNull(limitSize) && rowCount >= limitSize)) {
            dataList.add(data);
        }
        rowCount++;

        // 达到BATCH_COUNT了,需要去清一次数据,防止数据几万条数据在内存,容易OOM
        if (dataList.size() >= BATCH_SIZE) {

            // 存储完成清理 list
            dataList.clear();
        }
    }

    @Override
    public void doAfterAllAnalysed(AnalysisContext analysisContext) {
        log.info("所有数据解析完成!");
    }
 }

每批解析出的数据复杂处理

和demo中的存储逻辑一样,在解析完一批次数据之后,统一对这一批数据进行处理,然后清除。

public class ParseExcelListener extends AnalysisEventListener<Map<Integer, String>> {

    private Map<Integer, Map<Integer, String>> dataMap = Maps.newHashMap();
    private List<Map<Integer, String>> headList = new ArrayList<>();
    private ExcelDataHandler<Map<Integer, String>> dataHandler;
    private int rowId = 1;


    public ParseExcelListener(ExcelDataHandler<Map<Integer, String>> dataHandler) {
        this.dataHandler = dataHandler;
    }

    @Override
    public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
        headList.add(headMap);
    }

    @Override
    public void invoke(Map<Integer, String> integerStringMap, AnalysisContext analysisContext) {
        dataMap.put(rowId, integerStringMap);
        rowId++;

        // 达到BATCH_COUNT了,需要去清一次数据,防止数据几万条数据在内存,容易OOM
        if (dataMap.size() >= dataHandler.getHandlerBatch()) {

            dataHandler.actuator(dataMap, headList);
            // 存储完成清理 list
            dataMap.clear();
        }
    }

    @Override
    public void doAfterAllAnalysed(AnalysisContext analysisContext) {
        log.info("所有数据解析完成!");
        dataHandler.actuator(dataMap, headList);
    }

}

 		ParseExcelListener parseExcelListener = new ParseExcelListener(new ExcelDataHandler<Map<Integer, String>>() {
            /**
             * @Param: Map<行号, Map < 列号, 单元格内容>>
             */
            @Override
            public void actuator(Map<Integer, Map<Integer, String>> dataMap, List<Map<Integer, String>> headList) {
               //数据处理
            }
        });
        try {
            EasyExcelFactory.read(inputStream).registerReadListener(parseExcelListener).headRowNumber(1).sheet().doRead();
        } catch (Exception e) {
            log.warn("解析数据异常,文件url:{},错误原因:{}", importContext.getImportsRecord().getOriginUrl(), e);
            throw e;
        }

边读边写,如何追加写入

读是分批读,每一批有一个批量处理数据的方法,可以在批量处理数据时将读取到的数据处理后写入新文件中。
先看demo:

    /**
     * 重复多次写入
     * 

* 1. 创建excel对应的实体对象 参照{@link ComplexHeadData} *

* 2. 使用{@link ExcelProperty}注解指定复杂的头 *

* 3. 直接调用二次写入即可 */ @Test public void repeatedWrite() { // 方法1: 如果写到同一个sheet String fileName = TestFileUtil.getPath() + "repeatedWrite" + System.currentTimeMillis() + ".xlsx"; // 这里 需要指定写用哪个class去写 try (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(); excelWriter.write(data, writeSheet); } } // 方法2: 如果写到不同的sheet 同一个对象 fileName = TestFileUtil.getPath() + "repeatedWrite" + System.currentTimeMillis() + ".xlsx"; // 这里 指定文件 try (ExcelWriter excelWriter = EasyExcel.write(fileName, DemoData.class).build()) { // 去调用写入,这里我调用了五次,实际使用时根据数据库分页的总的页数来。这里最终会写到5个sheet里面 for (int i = 0; i < 5; i++) { // 每次都要创建writeSheet 这里注意必须指定sheetNo 而且sheetName必须不一样 WriteSheet writeSheet = EasyExcel.writerSheet(i, "模板" + i).build(); // 分页去数据库查询数据 这里可以去数据库查询每一页的数据 List<DemoData> data = data(); excelWriter.write(data, writeSheet); } } // 方法3 如果写到不同的sheet 不同的对象 fileName = TestFileUtil.getPath() + "repeatedWrite" + System.currentTimeMillis() + ".xlsx"; // 这里 指定文件 try (ExcelWriter excelWriter = EasyExcel.write(fileName).build()) { // 去调用写入,这里我调用了五次,实际使用时根据数据库分页的总的页数来。这里最终会写到5个sheet里面 for (int i = 0; i < 5; i++) { // 每次都要创建writeSheet 这里注意必须指定sheetNo 而且sheetName必须不一样。这里注意DemoData.class 可以每次都变,我这里为了方便 所以用的同一个class // 实际上可以一直变 WriteSheet writeSheet = EasyExcel.writerSheet(i, "模板" + i).head(DemoData.class).build(); // 分页去数据库查询数据 这里可以去数据库查询每一页的数据 List<DemoData> data = data(); excelWriter.write(data, writeSheet); } } }


        //本地新建一个临时文件
        File file = FileUtil.create(filePath, fileName);
        ExcelWriter excelWriter = EasyExcelFactory.write(file).build();
        WriteSheet writeSheet = EasyExcelFactory.writerSheet().build();
       
        ParseExcelListener parseExcelListener = new ParseExcelListener(new ExcelDataHandler<Map<Integer, String>>() {
            /**
             * @Param: Map<行号, Map < 列号, 单元格内容>>
             */
            @Override
            public void actuator(Map<Integer, Map<Integer, String>> dataMap, List<Map<Integer, String>> headList) {
            	//每批数据统一处理
            	//判断临时文件是否已存在,已存在则不需要表头,直接往后面追加写
            	if (!file.exists() || file.length() == 0) {
            		writeSheet.setHead(getHeader(importContext.getMapping(), excelHeadIdxNameMap));
        		}
        		excelWriter.write(importSafetyInventoryResults, writeSheet);
            }
        });
        try {
            EasyExcelFactory.read(inputStream).registerReadListener(parseExcelListener).headRowNumber(1).sheet().doRead();
        } catch (Exception e) {
            log.warn("解析数据异常,文件url:{},错误原因:{}", importContext.getImportsRecord().getOriginUrl(), e);
            throw e;
        } finally {
        	//所有数据解析完再关闭excelWriter,否则每批数据都会覆盖前面的内容
            if (excelWriter != null) {
                excelWriter.finish();
            }
        }
		//处理完需要把本地的临时文件删掉
        FileUtil.clearFile(file);

鸣谢

https://easyexcel.opensource.alibaba.com/docs/current/quickstart/read#%E4%B8%8D%E5%88%9B%E5%BB%BA%E5%AF%B9%E8%B1%A1%E7%9A%84%E8%AF%BB
https://blog.csdn.net/u012751272/article/details/126298071

你可能感兴趣的:(java)