前言
本文主要解决大数据量的复杂合并单元格的性能问题,包括时间效率和资源使用效率,同时会给出EasyExcel学习到生产级(十万数据以上合并单元格)全过程,同时还结合springcache写了字典表自动转换字段,希望后来者能更快上手这款强大的工具
前因概要
因为是2b系统,项目中多是导出的功能,导入功能对数据量的要求不会很大。原本项目中的工具类我写过一套poi的实现,实现了自动合并单元格和字典值转换功能。但一直会存在导出效率低,内存占用大的问题,所以后面又写了EasyExcel的实现工具类。(后面发现是合并的逻辑没写好,实际SXSSFWorkbook导出效率没这么低)
熟悉学习EasyExcel路线记录
EasyExcel官方文档 - 基于Java的Excel处理工具 | Easy Excel
EasyExcel的官网文档说的不是很全面,只是把基础的使用写了一下,要了解清楚,用明白还是需要github下载源码,debugger分析
这个帖子对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源码-实例会后续发