POI使用多线程导出多sheet页的Excel
一、前言
最近公司有个业务,需要导出多个sheet页的Excel,其中包括一个汇总sheet和8个子sheet,如下图所示。因为每个子sheet数据量可能达到1W以上,随着业务拓展,以后可能sheet会更多,因此这里需要使用多线程操作sheet导出Excel。网上查了很多资料,很多博主说POI不支持多线程操作sheet,但我就是实现了。
本文主要讲述使用多线程进行多个sheet的同时操作,这是宝贵的经验,留给自己以后参详,也留给广大网友做个参考
二、实现方式
由于我的业务功能限制,所以需要先准备好Excel模板,具体实现方式是,先读取excel模板中的sheet,业务中有几个sheet就循环几次,每次循环都复制第一个sheet模板页,然后使用多线程插入数据,最后删除第一个sheet页,比如:我先读取我excel模板的第一个sheet,需要做8个sheet页的导出,就循环8次,每次循环将第一个sheet复制一份出来,一共复制出来8份,最后删除第一个模板页就好了。
二、主体业务
废话不多说了,上代码
// 在类的外部声明一个原子整型变量,用来作为计数器
//提交线程任务使用的lambda表达式,lambda表达式属于内部类
//所以在外部定义int sum = 0;的话,表达式内部无法对sum进行累加
AtomicInteger cylinderNum = new AtomicInteger(0);
//定义一个长度为8的固定长度线程池。
ExecutorService executorService= Executors.newFixedThreadPool(8);
//开启一个countDownLatch计数器,等到各线程执行完成后再执行main线程
CountDownLatch countDownLatch= new CountDownLatch(fillReportBo.getStationIds().size());
for (int i=0; i<8; i++) {
List<FillDataInfoVo> fillInfoList = fillReportMapper.getFillListDataInfo(fillReportBo);
// 记录每次循环中数据的总量,使用原子整型变量的方法进行操作,保证线程安全
cylinderNum.addAndGet(fillInfoList.size());
// 复制一个子sheet工作表
//这里十分重要,复制sheet一定要在发起线程任务之外,也就是一定要在主线程去操作,不能放在子线程里
XSSFSheet sheetData = workbook.cloneSheet(1, "sheet" + (i+1));
//这里是提前定义好的表格样式
CellStyle finalStyleHeader = styleHeader;
CellStyle finalStyleBody = styleBody;
Font finalFontBody = fontBody;
//发起线程任务,处理每个sheet的数据
executorService.submit(() -> {
try {
//设置表头第一行
XSSFRow headerRowInfoOne = sheetData.getRow(0);
headerRowInfoOne.getCell(0).setCellValue(stationInfo.getStationShort() + "明细");
//设置表头第二行
XSSFRow headerRowInfoTwo = sheetData.getRow(1);
BigDecimal fillSumInfo = fillInfoList.stream()
.map(FillDataInfoVo::getFillNum)
.reduce(BigDecimal.ZERO, BigDecimal::add);
headerRowInfoTwo.getCell(1).setCellValue(fillSumInfo + " KG");
headerRowInfoTwo.getCell(1).setCellStyle(finalStyleHeader);
headerRowInfoTwo.getCell(3).setCellValue(fillReportBo.getFillStartTime() + "~" + fillReportBo.getFillEndTime());
headerRowInfoTwo.getCell(3).setCellStyle(finalStyleHeader);
设置表头第三行
XSSFRow headerRowInfoThree = sheetData.getRow(2);
Map<String, List<FillDataInfoVo>> devMap = StreamUtils.groupByKey(fillInfoList, FillDataInfoVo::getDevNo);
headerRowInfoThree.getCell(1).setCellValue(devMap.size());
headerRowInfoThree.getCell(1).setCellStyle(finalStyleHeader);
headerRowInfoThree.getCell(3).setCellValue(fillInfoList.size());
headerRowInfoThree.getCell(3).setCellStyle(finalStyleHeader);
//遍历数据集,将每条数据插入excel
for (FillDataInfoVo fillInfo : fillInfoList) {
int indexSheet = fillInfoList.indexOf(fillInfo);
//获取数据行
XSSFRow row = sheetData.getRow(indexSheet + 4);
if(StringUtils.isNull(row)){
row = sheetData.createRow(indexSheet + 4);
}
for (int i = 0; i < 4; i++) {
row.createCell(i);
}
// 取消行的自定义高度
CTRow ctRow = row.getCTRow();
ctRow.setCustomHeight(false);
setCell(row,fillInfo);
//设置本行所有单元格样式
getBodyStyle(row, finalStyleBody, finalFontBody);
}
countDownLatch.countDown();
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
}
});
}
//这步一定要有,主要是为了等待所有线程执行完成
countDownLatch.await();
setCell方法主要是对当前循环行里的每一列插入数据,但是因为多线程操作的,每个子线程都在抢夺CPU资源,因此这里需要加锁,否则会出现导出数据错乱的bug
public synchronized void setCell(XSSFRow row, FillDataInfoVo fillInfo){
row.getCell(0).setCellValue(StringUtils.isEmpty(fillInfo.getFillTime()) ? "" : fillInfo.getFillTime());
row.getCell(1).setCellValue(StringUtils.isEmpty(fillInfo.getStationName()) ? "" : fillInfo.getStationName());
row.getCell(2).setCellValue(StringUtils.isEmpty(fillInfo.getDevNo()) ? "" : fillInfo.getDevNo());
row.getCell(3).setCellValue(StringUtils.isNull(fillInfo.getFillNum()) ? "" : String.valueOf(fillInfo.getFillNum()));
}