公司项目最近有一个须要:报表导出。整个系统下来,起码超过一百张报表须要导出。这个时候如何优雅的实现报表导出,释放生产力就显得很重要了。下面主要给你们分享一下该工具类的使用方法与实现思路。java
对于每一个报表都相同的操做,咱们很天然的会抽离出来,这个很简单。而最重要的是:如何把那些每一个报表不相同的操做进行良好的封装,尽量的提升复用性;针对以上的原则,主要实现了一下关键功能点:git
上面说到了本工具类实现了三个功能点,天然在使用的时候设置好这三个要点便可:github
下面的export
函数能够直接向客户端返回一个excel数据,其中productInfoPos
为待导出的数据列表,ExcelHeaderInfo
用来保存表头信息,包括表头名称,表头的首列,尾列,首行,尾行。由于默认导出的数据格式都是字符串型,因此还须要一个Map参数用来指定某个字段的格式化类型(例如数字类型,小数类型、日期类型)。这里你们知道个大概怎么使用就行了,下面会对这些参数进行详细解释。spring
@Override
public void export(HttpServletResponse response, String fileName) {
// 待导出数据
List<TtlProductInfoPo> productInfoPos = this.multiThreadListProduct();
ExcelUtils excelUtils = new ExcelUtils(productInfoPos, getHeaderInfo(), getFormatInfo());
excelUtils.sendHttpResponse(response, fileName, excelUtils.getWorkbook());
}
// 获取表头信息
private List<ExcelHeaderInfo> getHeaderInfo() {
return Arrays.asList(
new ExcelHeaderInfo(1, 1, 0, 0, "id"),
new ExcelHeaderInfo(1, 1, 1, 1, "商品名称"),
new ExcelHeaderInfo(0, 0, 2, 3, "分类"),
new ExcelHeaderInfo(1, 1, 2, 2, "类型ID"),
new ExcelHeaderInfo(1, 1, 3, 3, "分类名称"),
new ExcelHeaderInfo(0, 0, 4, 5, "品牌"),
new ExcelHeaderInfo(1, 1, 4, 4, "品牌ID"),
new ExcelHeaderInfo(1, 1, 5, 5, "品牌名称"),
new ExcelHeaderInfo(0, 0, 6, 7, "商店"),
new ExcelHeaderInfo(1, 1, 6, 6, "商店ID"),
new ExcelHeaderInfo(1, 1, 7, 7, "商店名称"),
new ExcelHeaderInfo(1, 1, 8, 8, "价格"),
new ExcelHeaderInfo(1, 1, 9, 9, "库存"),
new ExcelHeaderInfo(1, 1, 10, 10, "销量"),
new ExcelHeaderInfo(1, 1, 11, 11, "插入时间"),
new ExcelHeaderInfo(1, 1, 12, 12, "更新时间"),
new ExcelHeaderInfo(1, 1, 13, 13, "记录是否已经删除")
);
}
// 获取格式化信息
private Map<String, ExcelFormat> getFormatInfo() {
Map<String, ExcelFormat> format = new HashMap<>();
format.put("id", ExcelFormat.FORMAT_INTEGER);
format.put("categoryId", ExcelFormat.FORMAT_INTEGER);
format.put("branchId", ExcelFormat.FORMAT_INTEGER);
format.put("shopId", ExcelFormat.FORMAT_INTEGER);
format.put("price", ExcelFormat.FORMAT_DOUBLE);
format.put("stock", ExcelFormat.FORMAT_INTEGER);
format.put("salesNum", ExcelFormat.FORMAT_INTEGER);
format.put("isDel", ExcelFormat.FORMAT_INTEGER);
return format;
}
复制代码
哈哈,本身分析本身的代码,有点意思。因为不方便贴出太多的代码,你们能够先到github上clone源码,再回来阅读文章。✨源码地址✨ LZ使用的poi 4.0.1
版本的这个工具,想要实用海量数据的导出天然得使用SXSSFWorkbook
这个组件。关于poi的具体用法在这里我就很少说了,这里主要是给你们讲解如何对poi进行封装使用。sql
咱们重点看ExcelUtils
这个类,这个类是实现导出的核心,先来看一下三个成员变量。编程
private List list;
private List<ExcelHeaderInfo> excelHeaderInfos;
private Map<String, ExcelFormat> formatInfo;
复制代码
该成员变量用来保存待导出的数据。api
该成员变量主要用来保存表头信息,由于咱们须要定义多个表头信息,因此须要使用一个列表来保存,ExcelHeaderInfo
构造函数以下 ExcelHeaderInfo(int firstRow, int lastRow, int firstCol, int lastCol, String title)
数组
firstRow
:该表头所占位置的首行lastRow
:该表头所占位置的尾行firstCol
:该表头所占位置的首列lastCol
:该表头所占位置的尾行title
:该表头的名称该参数主要用来格式化字段,咱们须要预先约定好转换成那种格式,不能随用户本身定。因此咱们定义了一个枚举类型的变量,该枚举类只有一个字符串类型成员变量,用来保存想要转换的格式,例如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;
}
}
复制代码
该方法用来初始化表头,而建立表头最关键的就是poi中Sheet类的
addMergedRegion(CellRangeAddress var1)
方法,该方法用于单元格融合
。咱们会遍历ExcelHeaderInfo列表,按照每一个ExcelHeaderInfo的坐标信息进行单元格融合,而后在融合以后的每一个单元首行
和首列
的位置建立单元格,而后为单元格赋值便可,经过上面的步骤就完成了任意类型的表头设置。bash
// 建立表头
private void createHeader(Sheet sheet, CellStyle style) {
for (ExcelHeaderInfo excelHeaderInfo : excelHeaderInfos) {
Integer lastRow = excelHeaderInfo.getLastRow();
Integer firstRow = excelHeaderInfo.getFirstRow();
Integer lastCol = excelHeaderInfo.getLastCol();
Integer firstCol = excelHeaderInfo.getFirstCol();
// 行距或者列距大于0才进行单元格融合
if ((lastRow - firstRow) != 0 || (lastCol - firstCol) != 0) {
sheet.addMergedRegion(new CellRangeAddress(firstRow, lastRow, firstCol, lastCol));
}
// 获取当前表头的首行位置
Row row = sheet.getRow(firstRow);
// 在表头的首行与首列位置建立一个新的单元格
Cell cell = row.createCell(firstCol);
// 赋值单元格
cell.setCellValue(excelHeaderInfo.getTitle());
cell.setCellStyle(style);
sheet.setColumnWidth(firstCol, sheet.getColumnWidth(firstCol) * 17 / 12);
}
}
复制代码
在进行正文赋值以前,咱们先要对原始数据列表转换成字符串的二维数组,之因此转成字符串格式是由于能够统一的处理各类类型,以后有须要咱们再转换回来便可。
// 将原始数据转成二维数组
private String[][] transformData() {
int dataSize = this.list.size();
String[][] datas = new String[dataSize][];
// 获取报表的列数
Field[] fields = list.get(0).getClass().getDeclaredFields();
// 获取实体类的字段名称数组
List<String> columnNames = this.getBeanProperty(fields);
for (int i = 0; i < dataSize; i++) {
datas[i] = new String[fields.length];
for (int j = 0; j < fields.length; j++) {
try {
// 赋值
datas[i][j] = BeanUtils.getProperty(list.get(i), columnNames.get(j));
} catch (Exception e) {
LOGGER.error("获取对象属性值失败");
e.printStackTrace();
}
}
}
return datas;
}
复制代码
这个方法中咱们经过使用反射技术,很巧妙的实现了任意类型的数据导出(这里的任意类型指的是任意的报表类型,不一样的报表,导出的数据确定是不同的,那么在Java实现中的实体类确定也是不同的)。要想将一个List转换成相应的二维数组,咱们得知道以下的信息:
若是获取以上三个信息呢?
Field[] getDeclaredFields()
这个方法获取实体类的全部字段,从而间接知道一共有多少列反射
,你就至关于拥有了全世界,那还有什么作不到的呢。这里咱们没有直接使用反射,而是使用了一个叫作BeanUtils
的工具,该工具能够很方便的帮助咱们对一个实体类进行字段的赋值与字段值的获取。很简单,经过BeanUtils.getProperty(list.get(i), columnNames.get(j))
这一行代码,咱们就获取了实体list.get(i)
中名称为columnNames.get(j)
这个字段的值。list.get(i)
固然是咱们遍历原始数据的实体类,而columnNames
列表则是一个实体类全部字段名的数组,也是经过反射的方法获取到的,具体实现能够参考LZ的源代码。这里的正文指定是正式的表格数据内容,其实这一些没有太多的奇淫技巧,主要的功能在上面已经实现了,这里主要是进行单元格的赋值与导出格式的处理(主要是为了导出excel后能够进行方便的运算)。
// 建立正文
private void createContent(Row row, CellStyle style, String[][] content, int i, Field[] fields) {
List<String> columnNames = getBeanProperty(fields);
for (int j = 0; j < columnNames.size(); j++) {
if (formatInfo == null) {
row.createCell(j).setCellValue(content[i][j]);
continue;
}
if (formatInfo.containsKey(columnNames.get(j))) {
switch (formatInfo.get(columnNames.get(j)).getValue()) {
case "DOUBLE":
row.createCell(j).setCellValue(Double.parseDouble(content[i][j]));
break;
case "INTEGER":
row.createCell(j).setCellValue(Integer.parseInt(content[i][j]));
break;
case "PERCENT":
style.setDataFormat(HSSFDataFormat.getBuiltinFormat("0.00%"));
Cell cell = row.createCell(j);
cell.setCellStyle(style);
cell.setCellValue(Double.parseDouble(content[i][j]));
break;
case "DATE":
row.createCell(j).setCellValue(this.parseDate(content[i][j]));
}
} else {
row.createCell(j).setCellValue(content[i][j]);
}
}
}
复制代码
导出工具类的核心方法就差很少说完了,下面说一下关于多线程查询的问题。
理想很丰满,现实仍是有点骨感的。LZ虽然对50w的数据分别建立20个线程去查询,可是整体的效率并非50w/20,而是仅仅快了几秒钟,知道缘由的小伙伴能够给我留个言一块儿探讨一下。
下面先说说具体思路:由于多个线程之间是同时执行的,你不可以保证哪一个线程先执行完毕,可是咱们却得保证数据顺序的一致性。在这里咱们使用了Callable
接口,经过实现Callable
接口的线程能够拥有返回值,咱们获取到全部子线程的查询结果,而后合并到一个结果集中便可。那么如何保证合并的顺序呢?咱们先建立了一个FutureTask
类型的List,该FutureTask
的类型就是返回的结果集。
List<FutureTask<List<TtlProductInfoPo>>> tasks = new ArrayList<>();
复制代码
当咱们每启动一个线程的时候,就将该线程的FutureTask
添加到tasks
列表中,这样tasks列表中的元素顺序就是咱们启动线程的顺序。
FutureTask<List<TtlProductInfoPo>> task = new FutureTask<>(new listThread(map));
log.info("开始查询第{}条开始的{}条记录", i * THREAD_MAX_ROW, THREAD_MAX_ROW);
new Thread(task).start();
// 将任务添加到tasks列表中
tasks.add(task);
复制代码
接下来,就是顺序塞值了,咱们按顺序从tasks
列表中取出FutureTask
,而后执行FutureTask
的get()
方法,该方法会阻塞调用它的线程,知道拿到返回结果。这样一套循环下来,就完成了全部数据的按顺序存储。
for (FutureTask<List<TtlProductInfoPo>> task : tasks) {
try {
productInfoPos.addAll(task.get());
} catch (Exception e) {
e.printStackTrace();
}
}
复制代码
若是须要导出海量数据,可能会存在一个问题:接口超时
,主要缘由就是整个导出过程的时间太长了。其实也很好解决,接口的响应时间太长,咱们缩短响应时间不就能够了嘛。咱们使用异步编程
解决方案,异步编程的实现方式有不少,这里咱们使用最简单的spring中的Async
注解,加上了这个注解的方法能够立马返回响应结果。关于注解的使用方式,你们能够本身查阅一下,下面讲一下关键的实现步骤:
到服务器本地
,这个时候就能够请求该接口直接下载文件了。这样就能够解决接口超时的问题了。
CREATE TABLE `ttl_product_info` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '记录惟一标识',
`product_name` varchar(50) NOT NULL COMMENT '商品名称',
`category_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '类型ID',
`category_name` varchar(50) NOT NULL COMMENT '冗余分类名称-避免跨表join',
`branch_id` bigint(20) NOT NULL COMMENT '品牌ID',
`branch_name` varchar(50) NOT NULL COMMENT '冗余品牌名称-避免跨表join',
`shop_id` bigint(20) NOT NULL COMMENT '商品ID',
`shop_name` varchar(50) NOT NULL COMMENT '冗余商店名称-避免跨表join',
`price` decimal(10,2) NOT NULL COMMENT '商品当前价格-属于热点数据,并且价格变化须要记录,须要价格详情表',
`stock` int(11) NOT NULL COMMENT '库存-热点数据',
`sales_num` int(11) NOT NULL COMMENT '销量',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '插入时间',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`is_del` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '记录是否已经删除',
PRIMARY KEY (`id`),
KEY `idx_shop_category_salesnum` (`shop_id`,`category_id`,`sales_num`),
KEY `idx_category_branch_price` (`category_id`,`branch_id`,`price`),
KEY `idx_productname` (`product_name`)
) ENGINE=InnoDB AUTO_INCREMENT=15000001 DEFAULT CHARSET=utf8 COMMENT='商品信息表';
复制代码
本次文章就写到这里啦,喜欢的朋友能够点赞、评论、加关注哦!