EasyExcel大数据量自动合并单元格导出解决方案

前言

本文主要解决大数据量的复杂合并单元格的性能问题,包括时间效率和资源使用效率,同时会给出EasyExcel学习到生产级(十万数据以上合并单元格)全过程,同时还结合springcache写了字典表自动转换字段,希望后来者能更快上手这款强大的工具

前因概要

因为是2b系统,项目中多是导出的功能,导入功能对数据量的要求不会很大。原本项目中的工具类我写过一套poi的实现,实现了自动合并单元格和字典值转换功能。但一直会存在导出效率低,内存占用大的问题,所以后面又写了EasyExcel的实现工具类。(后面发现是合并的逻辑没写好,实际SXSSFWorkbook导出效率没这么低)

熟悉学习EasyExcel路线记录

EasyExcel官方文档 - 基于Java的Excel处理工具 | Easy Excel

EasyExcel的官网文档说的不是很全面,只是把基础的使用写了一下,要了解清楚,用明白还是需要github下载源码,debugger分析

EasyExcel组件原理浅析 - 知乎

这个帖子对excel有较为详细的解释

EasyExcel针对比较大的数据量合并策略_addmergedregionunsafe-CSDN博客这个帖子对合并单元格有很好的解决方案,主要重写了合并方法从业务层面判断,去除isSetMergeCells方法(有锁synchronized),这个方法到合并很多单元格后查询会越来越慢,效率提升显著。实测数据量57509条(基本每个都是合并)。原本489867ms,优化后5511ms,效率提升100倍

easyexcel导出实例-工具类

导出效果

用户编号和用户编号相关信息,会合并单元格

表号按照用户编号的合并单元格上再进行单元格合并

价格版本按照表号的合并单元格上再进行单元格合并

形成一个多层级的合并单元格

核心方法调用        

/**
 * 自定义过滤字段的导出excel
 *      * @param data 需要导出的数据集
 *      * @param fileName 需要导出的excel名
 * @param outputStream 输出流
 * @param data 需要导出的数据集
 * @param fileName 需要导出的excel名
 * @param excelClass 导出实体类
 * @param excludeColumnFieldIndexList 排除字段索引集合
 * @param mergeColumnIndexArray 合并字段索引集合
 * @param rowMergeArray 合计、小计 new String[]{"合计"吗,"小计"}
 */
public static void exportByExcel(OutputStream outputStream,List<?> data, String fileName, Class<?> excelClass,
                                 List<Integer> excludeColumnFieldIndexList, Integer[] mergeColumnIndexArray,
                                 String[] rowMergeArray){
    //处理response,让返回MIME是excel类型 后面的异常都会打印到异常excel错误导出文件中
    exportValidateAndPrepare(fileName,outputStream);
    //核心导出逻辑
    try {
        long exportStartTime = System.currentTimeMillis();
        log.info("报表导出Size: " + data.size() + "条。");
        // 把查询到的数据按设置的sheet的容量进行切割,有些公司的电脑不行,execl分多个sheet
        List<? extends List<?>> lists = SplitList.splitList(data, PAGE_SIZE);
        List<String> fileds = new ArrayList<>(AbstractEasyExcel.getIncludeColumnFiledNames(excelClass));
        System.out.println("fileds================"+fileds);

        ExcelWriterBuilder excelWriterBuilder = EasyExcel.write(outputStream, excelClass)
                .registerWriteHandler(AbstractEasyExcel.formatExcel())
                .registerWriteHandler(new AbstractEasyExcel.ExcelWidthStyleStrategy());
        if(CollUtil.isNotEmpty(excludeColumnFieldIndexList)){//排除字段索引集合
            log.info("excludeColumnFieldIndexList:"+excludeColumnFieldIndexList);
            excelWriterBuilder.excludeColumnIndexes(excludeColumnFieldIndexList);
        }
        if(Objects.nonNull(mergeColumnIndexArray) && mergeColumnIndexArray.length>0){//合并字段索引集合
            log.info("mergeColumnIndexArray:"+Arrays.asList(mergeColumnIndexArray));
            excelWriterBuilder.registerWriteHandler(new CellMergeStrategy(data.size(),fileds.size(),mergeColumnIndexArray));
        }
        if(Objects.nonNull(rowMergeArray) && rowMergeArray.length>0){//rowMergeArray 合计、小计 new String[]{"合计"吗,"小计"}
            log.info("rowMergeArray:"+Arrays.asList(rowMergeArray));
            excelWriterBuilder.registerWriteHandler(new RowMergeStrategy(data.size(),fileds.size(),rowMergeArray));
        }
        if(CollUtil.isEmpty(lists)){//输出只有标题的空文件
            excelWriterBuilder.sheet("sheet").doWrite(data);
        }else{
            ExcelWriter excelWriter = excelWriterBuilder.build();
            ExcelWriterSheetBuilder excelWriterSheetBuilder;
            WriteSheet writeSheet;
            for (int i = 1; i <= lists.size(); ++i) {
                excelWriterSheetBuilder = new ExcelWriterSheetBuilder(excelWriter);
                excelWriterSheetBuilder.sheetNo(i).sheetName("sheet" + i);
                writeSheet = excelWriterSheetBuilder.build();
                excelWriter.write(lists.get(i - 1), writeSheet);
            }
            // 必须要finish才会写入,不finish只会创建empty的文件
            excelWriter.finish();
        }
        log.info("报表导出开始时间:"+new Date(exportStartTime)+";报表导出结束时间:" + new Date() + ";" +
                "导出耗时: " + (System.currentTimeMillis() - exportStartTime) + "ms;导出数量:"+data.size());
    } catch (Exception e) {
        log.error("报表导出异常:"+e);
        //excel导出文件中写入异常形象,因为在excel导出时报的错,干脆写在文档里了
        exportExceptionExcel(e,outputStream);
    }finally {
        try {
            // 关闭连接,释放资源
            if (outputStream != null) {
                outputStream.close();
            }
        } catch (Exception e2) {
            log.info("导出模块关闭输出流异常"+e2);
        }
    }
}

合并单元格子的处理handler

public class CellMergeStrategy implements RowWriteHandler {

    //要合并的列 从0开始
    private Set<Integer> mergeColumnIndexs;

    //合并行计数(用户辅助合并单元格)
    private int[] mergeCountArray;

    //总数据行数(用与判断最后一行,最后一行需要合并单元格)
    private Integer dateLength;

    //总输出字段数量(用于构建合并单元格一维数组初始化长度)
    private Integer fieldCount;

//    private Row lastRow;//做上个单元格缓存 用户判断单元格合并
    private Object[] lastCells;//缓存 如果是标题行 无值 后续都会把值存到这个数组中

    private int mergedTotalCount = 0;

    private CTWorksheet worksheet;
    /**
     * 合并单元格逻辑
     * @param writeSheetHolder
     * @param writeTableHolder Nullable.It is null without using table writes.
     * @param row
     * @param relativeRowIndex easyexcel使用table去写入时,每个table的relativeRowIndex都是从0开始,对于动态的数据多个table写入时,
     *                         当不确定某个table的rowIndex的具体值时,此参数就派上了用场
     * @param isHead           Nullable.It is null in the case of fill data.
     * 新建一维数组辅助确定上个关联单元格是否已经合并
     *                         只子啊
     */
    @Override
    public void afterRowDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row, Integer relativeRowIndex, Boolean isHead) {

        //当前行索引 第一行标题不合并
        int curRowNum = row.getRowNum();
        if(curRowNum%100==0){
            log.info("报表正在导出curRowNum:"+curRowNum);
        }

        if (CollUtil.isEmpty(mergeColumnIndexs) || isHead
                || Objects.isNull(dateLength) || dateLength==0
                || Objects.isNull(fieldCount) || fieldCount==0
        ) {
            return;
        }

        Iterator<Integer> iterator = mergeColumnIndexs.iterator();
        Boolean colIsMergeBool = true;//每行一开始都是false 都可以合并行+1 当有个格子合并了,后面不用判断都不能合并行+1,只能合并或者不合并
        while(iterator.hasNext()){
            Integer mergeIndex = iterator.next();
            //当前行单元格
            Cell curA1Cell = row.getCell(mergeIndex);
            Object curA1Data = curA1Cell.getCellTypeEnum() == CellType.STRING ? curA1Cell.getStringCellValue() : curA1Cell.getNumericCellValue();

            //上一行单元格
            Object preA1Data = lastCells[mergeIndex];
            if(curRowNum<dateLength && colIsMergeBool){//不是最后一行 且可合并标志位true
//                if(colIsMergeBool){
                    if(Objects.equals(curA1Data,preA1Data)){//相等
//                        colIsMergeBool=true;
                        mergeCountArray[mergeIndex]++;
                        continue;
                    }else{
                        colIsMergeBool=false;
                    }
            }
            if(mergeCountArray[mergeIndex]>1){//其他都是合并:1、最后一行合并 2、不是最后一行,上个值不等合并 3、不是最后一行,上个值相等合并 父已合并
                //按列合并单元格,合并后重置mergeCountArray
                mergeSomeRow3(writeSheetHolder,curRowNum,mergeCountArray[mergeIndex],mergeIndex);
            }
            
            lastCells[mergeIndex]=curA1Data;
        }
    }
    
    /**
     * 重写了底层合并单元格的实现,避开了四个校验,性能和安全还是要按照实际取舍
     * @param writeSheetHolder
     * @param curRowIndex
     * @param needMergeNum
     * @param curColIndex
     *
     * 参考https://blog.csdn.net/caicaizhangwei/article/details/123636959#comments_30931978
     */
    private void mergeSomeRow3(WriteSheetHolder writeSheetHolder, int curRowIndex, int needMergeNum, int curColIndex) {
        Sheet sheet = writeSheetHolder.getSheet();
        //firstRow=curRowIndex-1-(needMergeNum-1)
        try {
            CellRangeAddress cellAddresses = new CellRangeAddress(curRowIndex - needMergeNum, curRowIndex-1, curColIndex, curColIndex);
            if(worksheet==null){
                Field sh = sheet.getClass().getDeclaredField("_sh");
                sh.setAccessible(true);
                XSSFSheet shSheet = (XSSFSheet)sh.get(sheet);
                worksheet = shSheet.getCTWorksheet();
            }
            CTMergeCells ctMergeCells = mergedTotalCount > 0 ? worksheet.getMergeCells() : worksheet.addNewMergeCells();
            CTMergeCell ctMergeCell = ctMergeCells.addNewMergeCell();
            ctMergeCell.setRef(cellAddresses.formatAsString());
            mergedTotalCount ++;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

//        int mergeIndex = sheet.addMergedRegionUnsafe(cellAddresses);
        mergeCountArray[curColIndex] = 1;//合并后重置
    }

    public CellMergeStrategy(Integer dateLength, Integer fieldCount,Integer[] mergeColumnIndexArray) {
        Arrays.sort(mergeColumnIndexArray);
        this.mergeColumnIndexs = new LinkedHashSet<>(Arrays.asList(mergeColumnIndexArray));//因为需要排序后顺序读取
        log.info("mergeColumnIndexs初始化:"+this.mergeColumnIndexs);

        //初始化mergeCountArray
        mergeCountArray = new int[fieldCount];//所有导出字段长度就是合并一维数组长度
        Iterator<Integer> iterator = mergeColumnIndexs.iterator();
        while(iterator.hasNext()){
            Integer mergeIndex = iterator.next();
//            int mergeIndex = mergeColumnIndexs[i];
            mergeCountArray[mergeIndex]=1;//除合并列是1,其他都是初始化的0
        }
        log.info("mergeCountArray:"+new ArrayList<>(Arrays.asList(mergeCountArray)));

        this.dateLength = dateLength;
        log.info("dateLength:"+dateLength);
        this.fieldCount = fieldCount;
        log.info("fieldCount:"+fieldCount);

        lastCells = new Object[fieldCount];
    }
}

github源码-实例会后续发

  • 7
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值