EasyExcel合并行处理并优化

业务场景

由于业务需要导出如下图中订单数据和订单项信息,而一个订单对应多个订单项,所以会涉及到自定义合并行
最终效果图

1.简单处理

项目使用的EasyExcel,经查找发现Excel种有个AbstractMergeStrategy抽象类,可以用于合并单元格。
于是先简单的写一个工具类实现根据数据自定义合并单元行,基本思路是获取当前单元格内容和上一行的单元格内容比对,如果相同则添加合并区域,如果不同则不处理,如果上一个单元格在合并区域中,则先移除合并区域再将当前单元格添加到合并区域中。

public class RowMergeStrategy extends AbstractMergeStrategy {
	@Override
    protected void merge(final Sheet sheet, final Cell cell, final Head head, final Integer relativeRowIndex) {
    	//判断是否为需要合并的列
        if (...) {
            mergeWithPrevRow(sheet, cell, cell.getRowIndex(), cell.getColumnIndex());
        }
    }
    
	/**
     * 合并行
     *
     * @param sheet       当前sheet
     * @param cell        用于比对的合并单元格(可以指定根据其他列的单元格合并,也可以根据当前单元格和上一个单元格数据自动合并)
     * @param curRowIndex 当前单元格行索引
     * @param curColIndex 当前单元格列索引
     */
    private void mergeWithPrevRow(Sheet sheet, Cell cell, int curRowIndex, int curColIndex) {
        //获取当前行的当前列的数据和上一行的当前列列数据,通过上一行数据是否相同进行合并
        int preRowIndex = curRowIndex - 1;
        Row preRow = cell.getSheet().getRow(preRowIndex);
        Cell preCell = preRow.getCell(cell.getColumnIndex());

        // 1 比较当前行的 单元格 与上一行的单元格是否相同
        if (Objects.equals(getCellData(cell), getCellData(preCell))) {
            List<CellRangeAddress> mergeRegions = sheet.getMergedRegions();
            // 2 查看上一行的单元格是否在合并区域中
            Optional<CellRangeAddress> optional = mergeRegions.stream()
                .filter(c -> c.isInRange(preRowIndex, curColIndex)).findFirst();

            CellRangeAddress address;
            if (optional.isPresent()) {
           		//3.1 如果单元格在合并区域中,则先删除原合并区域,再将合并区域下拉一行
                address = optional.get();
                sheet.removeMergedRegion(mergeRegions.indexOf(address));
                address.setLastRow(curRowIndex);
            } else {
            	//3.2 如果单元格不在合并区域中,则将当前单元格和上一单元格组成合并区域
                address = new CellRangeAddress(preRowIndex, curRowIndex, curColIndex, curColIndex);
            }
            // 4 将合并区域区域加入sheet中
            sheet.addMergedRegion(address);
        }
    }
}    

2.优化数据显示

第一个版本上线后财务反馈数据有问题。如图,求和数值实际应该是45,但是显示为90,导致财务不好对账
1.0版本缺陷
经排查发现图中C3、C5单元格虽然已经合并了,但是数据仍然存在,导致下拉选中的时候将他们的值也计算进去了。于是在1.0的基础上调整,将合并单元格仅保留首行数据,其他行内容直接清空

	/**
     * 合并行
     *
     * @param sheet       当前sheet
     * @param cell        用于比对的合并单元格(可以指定根据其他列的单元格合并,也可以根据当前单元格和上一个单元格数据自动合并)
     * @param curRowIndex 当前单元格行索引
     * @param curColIndex 当前单元格列索引
     */
	private void mergeWithPrevRow(Sheet sheet, Cell cell, int curRowIndex, int curColIndex) {
	    //获取当前行的当前列的数据和上一行的当前列列数据,通过上一行数据是否相同进行合并
	    int preRowIndex = curRowIndex - 1;
	    List<CellRangeAddress> mergeRegions = cell.getSheet().getMergedRegions();
	    //1 查询当前单元格上一行的单元格是否为合并单元格
	    Optional<CellRangeAddress> optional = mergeRegions.stream()
	            .filter(c -> c.isInRange(preRowIndex, curColIndex)).findFirst();
	
	    CellRangeAddress address;
	    if (optional.isPresent()) {
	    	//2.1 上一行是合并单元格,则取合并单元格第一行的数据比对
	        address = optional.get();
	        Row firstRow = sheet.getRow(address.getFirstRow());
	        Cell firstCell = firstRow.getCell(cell.getColumnIndex());
	        if (Objects.equals(getCellData(cell), getCellData(firstCell))) {
	            sheet.removeMergedRegion(mergeRegions.indexOf(address));
	            address.setLastRow(curRowIndex);
	            sheet.addMergedRegion(address);
	            //匹配成功清空当前单元格
	            cell.setBlank();
	        }
	    } else {
	    	//2.2 上一行不是合并单元格,则取上一行单元格数据比对
	        Row preRow = sheet.getRow(preRowIndex);
	        Cell preCell = preRow.getCell(cell.getColumnIndex());
	        if (Objects.equals(getCellData(cell), getCellData(preCell))) {
	            address = new CellRangeAddress(preRowIndex, curRowIndex, curColIndex, curColIndex);
	            sheet.addMergedRegion(address);
	            //匹配成功清空当前单元格
	            cell.setBlank();
	        }
	    }
	}

3.优化合并策略

第二版上线后数据问题已经解决,但是财务反馈导出大量数据时太慢。本来打算调整为异步导出解决此问题,但是经过测试发现10000条数据如果不合并直接生成excel只要几秒,但是使用合并自定义合并策略就非常慢,需要一二十分钟,这个时间差大的太离谱了,于是查看合并策略代码有哪些地方可以优化的。
经过查看代码可以发现在上述2.1步骤中一直查找合并单元格数据,然后一直删除再新增。假设10000条数据,有3列需要自动合并,每三行合并,执行以上代码会执行6666✖️3次新增和3333✖️3次删除,大大的影响了效率。于是调整代码,处理数据时只保存需要合并的单元格信息,导出完成再统一添加合并信息到sheet

//此处需要实现WorkbookWriteHandler,保证文件写完后再添加合并区域到sheet
public class RowMergeStrategy extends AbstractMergeStrategy implements WorkbookWriteHandler

	/**
     * 文件写完后写入合并区域信息
     * @param context
     */
	@Override
    public void afterWorkbookDispose(final WorkbookWriteHandlerContext context) {
        //当前表格写完后,统一写入
        Map<Integer, List<CellRangeAddress>> rangeMap = ExcelThreadContext.getMap(RANGE_CONTEXT_KEY);
        if (CollUtil.isNotEmpty(rangeMap)) {
            rangeMap.values().stream().flatMap(list -> list.stream().filter(CollUtil::isNotEmpty))
                .forEach(address -> context.getWriteContext().writeSheetHolder().getSheet().addMergedRegion(address));
        }
        //清除线程中的数据
        ExcelThreadContext.clear();
    }
	
	private void mergeWithPrevCell(Sheet sheet, Cell cell, int curRowIndex, int curColIndex) {
        //ExcelThreadContext 就是一个ThreadLocal,用于缓存当前表格数据
        //用于存储每列的最后一个合并区域的首行单元格  key:列序号 value:最后一个合并区域首行单元格
        Map<Integer, Cell> cellMap = ExcelThreadContext.getMap(CONTEXT_KEY);
        //用于存储每列的所有合并单元格信息 key:列序号 value:合并单元格列表
        Map<Integer, List<CellRangeAddress>> rangeMap = ExcelThreadContext.getMap(RANGE_CONTEXT_KEY);

        int preRowIndex = curRowIndex - 1;
        //1 获取该列最后一个合并区域
        List<CellRangeAddress> addressList = rangeMap.get(curColIndex);
        CellRangeAddress address = this.getLastRangeAddress(addressList);

        //2.1 上一行在合并区域内,则取合并区域第一行的数据比对
        if (Objects.nonNull(address) && address.isInRange(preRowIndex, curColIndex)) {
            Cell firstCell = cellMap.get(curColIndex);
            if (Objects.equals(getCellData(cell), getCellData(firstCell))) {
            	//3.1 修改合并区域,将当前单元格加入合并区域 
                address.setLastRow(curRowIndex);
                cell.setBlank();
            }
        } else {
            //2.2 上一行不在合并区域内 比较当前单元格 与上一行是否内容相同
            Row preRow = sheet.getRow(preRowIndex);
            Cell preCell = preRow.getCell(cell.getColumnIndex());
            if (Objects.equals(getCellData(cell), getCellData(preCell))) {
            	//3.2 添加合并区域,将当前单元格和上一行合并,并将当前合并区域放入线程
                address = new CellRangeAddress(preRowIndex, curRowIndex, curColIndex, curColIndex);
                rangeMap.put(curColIndex, this.addRangeAddress(addressList, address));
                //将上一行单元格放入线程
                cellMap.put(curColIndex, preCell);
                cell.setBlank();
            }
        }
        ExcelThreadContext.setData(CONTEXT_KEY, cellMap);
        ExcelThreadContext.setData(RANGE_CONTEXT_KEY, rangeMap);
    }

	private List<CellRangeAddress> addRangeAddress(final List<CellRangeAddress> list, final CellRangeAddress address) {
        List<CellRangeAddress> addressList = list;
        if (CollUtil.isEmpty(addressList)) {
            addressList = new ArrayList<>();
        }
        addressList.add(address);
        return addressList;
    }

    private CellRangeAddress getLastRangeAddress(List<CellRangeAddress> list) {
        if (CollUtil.isEmpty(list)) {
            return null;
        }
        return list.get(list.size() - 1);
    }

经过测试,原本生成文件需要20分钟左右,现优化到只需要20多秒了。

工具类


/**
 * excel线程上下文.
 *
 */
public class ExcelThreadContext {

    private static final ThreadLocal<Map<String, Object>> THREAD_LOCAL = ThreadLocal.withInitial(HashMap::new);

    public static void clear() {
        THREAD_LOCAL.remove();
    }

    public static void setData(String key, Object value) {
        Map<String, Object> map = get();
        map.put(key, value);
    }

    public static Integer getInteger(String key) {
        return getInteger(key, 1);
    }

    public static Integer getInteger(String key, Integer defaultValue) {
        Map<String, Object> map = get();
        return Convert.toInt(map.get(key), defaultValue);
    }

    public static <K, V> Map<K, V> getMap(String key) {
        return getMap(key, new HashMap<>());
    }

    public static <K, V> Map<K, V> getMap(String key, Map<K, V> defaultValue) {
        Map<String, Object> map = get();
        try {
            return (Map<K, V>) map.getOrDefault(key, defaultValue);
        } catch (Exception e) {
            return defaultValue;
        }
    }

    public static <T> T getObject(String key) {
        return getObject(key, null);
    }

    public static <T> T getObject(String key, T defaultValue) {
        Map<String, Object> map = get();
        try {
            return (T) map.getOrDefault(key, defaultValue);
        } catch (Exception e) {
            return defaultValue;
        }
    }

    private static void set(Map<String, Object> map) {
        THREAD_LOCAL.set(map);
    }

    public static Map<String, Object> get() {
        return THREAD_LOCAL.get();
    }
}

因为数据是存储在线程中的,需要每次使用后清理线程数据


/**
 * 清理线程数据.
 *
 */
public class ClearExcelThreadHandler implements WorkbookWriteHandler {

    @Override
    public void afterWorkbookDispose(final WriteWorkbookHolder writeWorkbookHolder) {
        ExcelThreadContext.clear();
    }

    /**
     * 数据越小越靠前,默认值为0
     *  此处排在最后,防止其他业务没处理完就将线程数据清理掉了
     * @return
     */
    @Override
    public int order() {
        return Integer.MAX_VALUE;
    }
}


...

//使用方法
EasyExcel.write(file, clazz)
            .registerWriteHandler(new ClearExcelThreadHandler())
            .sheet()
            .doWrite(dataList);
  • 4
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 13
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值