写在前面
文章开始之前,请各位看官查阅EasyExcel简单用法。
EasyExcel.write(fileName)
// 这里放入动态头
.head(head()).sheet("模板")
// 当然这里数据也可以用 List<List<String>> 去传入
.doWrite(data());
head是List<List<String>>data是List<List<Object>>
根据官方文档,实现写Excel基本上只需要准备head和data,事实上也是如此,并且这个feature助于我们实现解藕。回想一下传统POI好像也是这种写法。
目标
基于多线程构造
data,输出多个文件,并压缩,基于redis记录导出进度,基于future实现下载中断,基于网络IO输出到客户端。
多线程构造data
- 准备一个线程池,java 线程池有四个,选哪个好。根据你的需求场景,我这边的场景是报表导出,报表导出是个频繁的需求,频率随太阳上升而超频,随太阳下降而降频,这么看来
newCachedThreadPool很适合。为什么?因为报表读多写少,基于缓存的线程池,帮助我们应对白天频繁的导出,夜晚导出变少之后缓存线程会回收,而夜晚有很多 job 会写入数据到报表系统。 - 不要用 JDK 自带线程池,尽管刚说 newCachedThreadPool 很适合报表,但没人会建议使用 JDK 自带线程池,至于为什么,被同事发现并打一顿就知道了。
缓冲流 ByteArrayOutputStream & # BufferedOutputStream
第一个要排除普通 IO 流,就那写一个字节输出一个字节的节奏,遇到几 M 的文件,得好久才能写完。 这两个流都起到缓冲数据,一次性输出缓存数据到客户端,减少数据在用户态和系统态之间 copy 的次数,提高数据输出效率
写不下去了,直接贴代码吧!
private long writeFile(OutputStream out, T condition, int fileNum, int sheetNum, Long exportId, long searchCount) throws IOException {
List<Future<List<List<Object>>>> futureList = new ArrayList<>(fileNum);
AtomicInteger executedCount = new AtomicInteger(0);
this.futureMap.put(exportId, futureList);
try (ByteArrayOutputStream bOut = new ByteArrayOutputStream()) {
IntStream.range(1, fileNum*sheetNum+1).boxed().forEach(sheetNo ->
futureList.add(this.taskPoolExecutor.submit(() -> this.loadSheet(condition, sheetNo))));
try (ZipOutputStream zOut = new ZipOutputStream(out)) {
ExecutorMeta executorMeta = ExecutorMeta.builder()
.bOut(bOut)
.futureList(futureList)
.exportId(exportId)
.executedCount(executedCount)
.searchCount(searchCount)
.head(this.head())
.sheetNum(sheetNum)
.function(this.stringRedisTemplate.opsForValue()::get)
.consumer((k,v) -> this.stringRedisTemplate.opsForValue().set(k,v))
.biFunction(this::percentage)
.build();
for (int fileNo = 0; fileNo < fileNum; fileNo++) {
executorMeta.setFileNo(fileNo);
EasyExcelExecutor.write(executorMeta);
this.zip(zOut, bOut, fileNo+1);
}
}
}
return executedCount.get();
}
以上是核心代码,逐个解析参数
- out http 的 response 里的 OutPutStream
- condition 是数据查询的条件
- fileNum 还记得我们的目标是导出多个文件吗,该值根据导出总数据量
searchCount大小和每个文件数据量pageSize大小算出来的 - sheetNum 每个 excel file 都有诺干个 sheet,该值通过
pageSize和sheetSize计算出来的 - exportId 每次导出请求需要落库,拿到 exportId 做一些事,例如基于 redis 的导出进度
下面解析代码
- futureList 还记得多线程查询数据吗,构建 future,后面遍历 future 拿到请求结果,这么做是为了实现导出中断
- executedCount 无需多说
- this.futureMap.put(exportId, futureList); 这一步和步骤一一起,为了后面导出中断时,通过 exportId 找到对于 future
- 后面的代码就是多线程加载数据,main 线程遍历 futureList ,调用 easyExcel 导出
- 剩下的看代码吧,我放到github github.com/Raidohub/re…
课后拓展
实现导出中断
- 将任务放到 future
- 执行任务过程,根据实际情况设置中断检查
sheetData = future.get();
if (sheetData.isEmpty()) {
return;
}
future 阻塞原理
基于 LockSupport,底层是 UNSAFE,AQS 就是基于这玩意。如果有兴趣可以继续学习 Thread.join() 原理做个对比。