一、引入
通过阅读本文,你将了解到:
- 使用EasyExcel写出Excel时,如何一步步提升性能;
- 写出有合并单元格的页签时,如何在更短的时间内写出更多的数据。
如果你第一次接触EasyExcel,可以先访问官网,按照指引掌握EasyExcel读写操作,然后再阅读本文。
如果你已经掌握了使用EasyExcel读写Excel,那么当你需要写出有合并单元格的大页签时,你会如何实现?
github地址:github.com/alibaba/eas…
1 | java复制代码<!-- 本文引用的版本 --> |
假设有个需求
假设你所在的公司需要开发一个功能:将数据库中票据表写出到Excel中,而且想在尽可能短的时间内(如30秒)写出几个月甚至一年内的数据(可能有几十万、上百万条记录),你会如何实现?
我们先来看看票据的一个简单模型:由一个头信息区、多条明细两部分组成,写出到Excel时样式如下。
1 | java复制代码import com.alibaba.excel.annotation.ExcelProperty; |
二、无合并单元格时
2.1 一次性查询写出
不考虑单元格合并时,你可能会这样实现:一次性查询所有数据,然后一次性写出。
1 | java复制代码 // 查询所有数据 |
这样实现有问题吗?数据量较少时没问题。
可是,当一次需要写出的数据有数万条甚至更多时,将所有数据一次性查询到内存中,当所有数据写出后,才能释放内存。这样可能导致很大的内存压力,甚至服务OOM。
有什么更好的办法吗?有。
2.2 分页查询写出
EasyExcel支持重复多次写单个或者多个Sheet页,我们可以多次分页查库获取数据,循环写入到一个Excel页签中。
1 | java复制代码 // 查询数据总量 |
现在,查询数据时内存压力减小了。可是等上线后,发现导出一个月的数据可能需要10秒,导出半年内数据时可能需要50秒或更长时间。如果是离线导出,耗时久点也能接受;如果是在线导出,可能接口响应超时。
有更高效的办法吗?
2.3 并发查询依次写出
查询一页数据写出后,再查询下一页,读与写是串行的。而且分页查询时页码越大,一次查询耗时越久。那么我们能否并发查询同时写出呢?当然可以。
1 | java复制代码 public static final ExecutorService EXECUTOR_SERVICE = Executors.newFixedThreadPool(4); |
三、合并单元格写出Excel
现在我们考虑如何实现合并单元格。因为一个票据有多个明细行(数量不确定),导出Excel时要将“票据编号”、“创建时间”等列跨行合并。该如何实现呢?
3.1 EasyExcel中实现
EasyExcel提供了两个创建合并单元格的注解,以及与注解等效的WriteHandler接口实现。定义下面的数据类,我们来试用一下。
1 | java复制代码@Getter |
3.1.1 @ContentLoopMerge
先不使用@ContentLoopMerge,生成的Excel如下:
对第一列使用@ContentLoopMerge后,生成的Excel如下:
1 | java复制代码 // 每两行合并一次,跨两列 |
3.1.2 @OnceAbsoluteMerge
该注解通过指定合并区域行列索引,用来创建一个合并区域(不是循环创建);单元格值取左上角单元格的。
1 | java复制代码@Getter |
效果如下:
3.1.3 WriteHandler实现
EasyExcel提供了与上面两个注解等效的WriteHandler实现,分别是OnceAbsoluteMergeStrategy
和LoopMergeStrategy
。使用方式如下:
1 | java复制代码public static void mergeWrite() { |
3.2 自定义合并策略
3.2.1 网络上的常规实现
票据导出时因为每个票据的明细行数量不定,@ContentLoopMerge就不适用了。此时,我们自然想到去网上找找方案。
比如,我找到了这篇博客《EasyExcel导出自定义合并单元格策略》https://cloud.tencent.com/developer/article/1671316
。
它的实现方式如下,核心逻辑为:实现CellWriteHandler接口,在Cell层面,每写一行数据,将合并列的单元格数据,与上一行的单元格数据比较。如果数据相同,就将当前行与上一行合并;如果上一行已被合并,则将当前行加入到合并区。
1 | java复制代码import com.alibaba.excel.metadata.Head; |
能实现我们导出票据的需求吗?能。但是试用后将会发现,这个实现性能不佳:
- 每写入一个单元格,都需要读取上一行,一边写入一边读取;
- 当上一行已经合并过了,本次写入需要修改合并区域,而且会反复修改;
- 比如,写出下图中第一个票据,写出3行,将读取3次,修改合并区域两次。
此外,网上还有一些基于RowWriteHandler接口的实现,也存在上面指出的性能问题。
3.2.2 我的实现
当我们分页查询票据记录后,可以按照合并自动进行分组,每组数量就是合并区域大小,合并区域位置可以通过行数累加来定位。因此。写出Excel前就可以预知那些合并区域。如果在创建sheet页时就将这些区域一并创建,写出时就不用关注单元格合并了。岂不美哉!
预创建合并区:实现SheetWriteHandler接口,重写afterSheetCreate(),将合并区域加入到sheet中。
1 | java复制代码import com.alibaba.excel.write.handler.SheetWriteHandler; |
1 | java复制代码 public static final ExecutorService EXECUTOR_SERVICE = Executors.newFixedThreadPool(4); |
该方式我已在工作中使用,性能确实有较大提升。感兴趣的小伙伴,也不妨一试。
本文转载自: 掘金