作者:你在我家门口来源:https://juejin.im/post/5c6b6b126fb9a04a0c2f024f
前言
公司项目最近有一个需要:报表导出。整个系统下来,起码超过一百张报表需要导出。这个时候如何优雅的实现报表导出,释放生产力就显得很重要了。下面主要给大家分享一下该工具类的使用方法与实现思路。
实现的功能点
对于每个报表都相同的操作,我们很自然的会抽离出来,这个很简单。而最重要的是:如何把那些每个报表不相同的操作进行良好的封装,尽可能的提高复用性;针对以上的原则,主要实现了一下关键功能点:
- 导出任意类型的数据
- 自由设置表头
- 自由设置字段的导出格式
使用实例
上面说到了本工具类实现了三个功能点,自然在使用的时候设置好这三个要点即可:
- 设置数据列表
- 设置表头
- 设置字段格式
下面的export函数可以直接向客户端返回一个excel数据,其中productInfoPos为待导出的数据列表,ExcelHeaderInfo用来保存表头信息,包括表头名称,表头的首列,尾列,首行,尾行。因为默认导出的数据格式都是字符串型,所以还需要一个Map参数用来指定某个字段的格式化类型(例如数字类型,小数类型、日期类型)。这里大家知道个大概怎么使用就好了,下面会对这些参数进行详细解释。
实现效果
源码分析
哈哈,自己分析自己的代码,有点意思。由于不方便贴出太多的代码,大家可以先到github上clone源码,再回来阅读文章。✨源码地址✨LZ使用的poi 4.0.1版本的这个工具,想要实用海量数据的导出自然得使用SXSSFWorkbook这个组件。关于poi的具体用法在这里我就不多说了,这里主要是给大家讲解如何对poi进行封装使用。
成员变量
我们重点看ExcelUtils这个类,这个类是实现导出的核心,先来看一下三个成员变量。
private List list; private List excelHeaderInfos; private Map formatInfo;
list
该成员变量用来保存待导出的数据。
ExcelHeaderInfo
该成员变量主要用来保存表头信息,因为我们需要定义多个表头信息,所以需要使用一个列表来保存,ExcelHeaderInfo构造函数如下ExcelHeaderInfo(int firstRow, int lastRow, int firstCol, int lastCol, String title)
- firstRow:该表头所占位置的首行
- lastRow:该表头所占位置的尾行
- firstCol:该表头所占位置的首列
- lastCol:该表头所占位置的尾行
- title:该表头的名称
ExcelFormat
该参数主要用来格式化字段,我们需要预先约定好转换成那种格式,不能随用户自己定。所以我们定义了一个枚举类型的变量,该枚举类只有一个字符串类型成员变量,用来保存想要转换的格式,例如FORMAT_INTEGER就是转换成整型。因为我们需要接受多个字段的转换格式,所以定义了一个Map类型来接收,该参数可以省略(默认格式为字符串)。
public enum ExcelFormat { FORMAT_INTEGER("INTEGER"), FORMAT_DOUBLE("DOUBLE"), FORMAT_PERCENT("PERCENT"), FORMAT_DATE("DATE"); private String value; ExcelFormat(String value) { this.value = value; } public String getValue() { return value; }}
核心方法
1. 创建表头
该方法用来初始化表头,而创建表头最关键的就是poi中Sheet类的addMergedRegion(CellRangeAddress var1)方法,该方法用于单元格融合。我们会遍历ExcelHeaderInfo列表,按照每个ExcelHeaderInfo的坐标信息进行单元格融合,然后在融合之后的每个单元首行和首列的位置创建单元格,然后为单元格赋值即可,通过上面的步骤就完成了任意类型的表头设置。
2. 转换数据
在进行正文赋值之前,我们先要对原始数据列表转换成字符串的二维数组,之所以转成字符串格式是因为可以统一的处理各种类型,之后有需要我们再转换回来即可。
这个方法中我们通过使用反射技术,很巧妙的实现了任意类型的数据导出(这里的任意类型指的是任意的报表类型,不同的报表,导出的数据肯定是不一样的,那么在Java实现中的实体类肯定也是不一样的)。要想将一个List转换成相应的二维数组,我们得知道如下的信息:
- 二维数组的列数
- 二维数组的行数
- 二维数组每个元素的值
如果获取以上三个信息呢?
- 通过反射中的Field[] getDeclaredFields()这个方法获取实体类的所有字段,从而间接知道一共有多少列
- List的大小不就是二维数组的行数了嘛
- 虽然每个实体类的字段名不一样,那么我们就真的无法获取到实体类某个字段的值了吗?不是的,你要知道,你拥有了反射,你就相当于拥有了全世界,那还有什么做不到的呢。这里我们没有直接使用反射,而是使用了一个叫做BeanUtils的工具,该工具可以很方便的帮助我们对一个实体类进行字段的赋值与字段值的获取。很简单,通过BeanUtils.getProperty(list.get(i), columnNames.get(j))这一行代码,我们就获取了实体list.get(i)中名称为columnNames.get(j)这个字段的值。list.get(i)当然是我们遍历原始数据的实体类,而columnNames列表则是一个实体类所有字段名的数组,也是通过反射的方法获取到的,具体实现可以参考LZ的源代码。
3. 赋值正文
这里的正文指定是正式的表格数据内容,其实这一些没有太多的奇淫技巧,主要的功能在上面已经实现了,这里主要是进行单元格的赋值与导出格式的处理(主要是为了导出excel后可以进行方便的运算)。
导出工具类的核心方法就差不多说完了,下面说一下关于多线程查询的问题。
多扯两点
1. 多线程查询数据
理想很丰满,现实还是有点骨感的。LZ虽然对50w的数据分别创建20个线程去查询,但是总体的效率并不是50w/20,而是仅仅快了几秒钟,知道原因的小伙伴可以给我留个言一起探讨一下。
下面先说说具体思路:因为多个线程之间是同时执行的,你不能够保证哪个线程先执行完毕,但是我们却得保证数据顺序的一致性。在这里我们使用了Callable接口,通过实现Callable接口的线程可以拥有返回值,我们获取到所有子线程的查询结果,然后合并到一个结果集中即可。那么如何保证合并的顺序呢?我们先创建了一个FutureTask类型的List,该FutureTask的类型就是返回的结果集。
List>> tasks = new ArrayList<>();
当我们每启动一个线程的时候,就将该线程的FutureTask添加到tasks列表中,这样tasks列表中的元素顺序就是我们启动线程的顺序。
FutureTask> task = new FutureTask<>(new listThread(map)); log.info("开始查询第{}条开始的{}条记录