前言
公司项目最近有一个需要:报表导出。整个系统下来,起码超过一百张报表需要导出。这个时候如何优雅的实现报表导出,释放生产力就显得很重要了。下面主要给大家分享一下该工具类的使用方法与实现思路。实现的功能点
对于每个报表都相同的操作,我们很自然的会抽离出来,这个很简单。而最重要的是:如何把那些每个报表不相同的操作进行良好的封装,尽可能的提高复用性;针对以上的原则,主要实现了一下关键功能点:导出任意类型的数据自由设置表头自由设置字段的导出格式使用实例
上面说到了本工具类实现了三个功能点,自然在使用的时候设置好这三个要点即可:设置数据列表设置表头设置字段格式下面的export函数可以直接向客户端返回一个excel数据,其中productInfoPos为待导出的数据列表,ExcelHeaderInfo用来保存表头信息,包括表头名称,表头的首列,尾列,首行,尾行。因为默认导出的数据格式都是字符串型,所以还需要一个Map参数用来指定某个字段的格式化类型(例如数字类型,小数类型、日期类型)。这里大家知道个大概怎么使用就好了,下面会对这些参数进行详细解释。@Overridepublicvoidexport(HttpServletResponse response, StringfileName) {// 待导出数据List<TtlProductInfoPo> productInfoPos = this.multiThreadListProduct();ExcelUtils excelUtils = newExcelUtils(productInfoPos, getHeaderInfo(), getFormatInfo());excelUtils.sendHttpResponse(response, fileName, excelUtils.getWorkbook());}// 获取表头信息privateList<ExcelHeaderInfo> getHeaderInfo() {returnArrays.asList(newExcelHeaderInfo(1, 1, 0, 0, “id”),newExcelHeaderInfo(1, 1, 1, 1, “商品名称”),newExcelHeaderInfo(0, 0, 2, 3, “分类”),newExcelHeaderInfo(1, 1, 2, 2, “类型ID”),newExcelHeaderInfo(1, 1, 3, 3, “分类名称”),newExcelHeaderInfo(0, 0, 4, 5, “品牌”),newExcelHeaderInfo(1, 1, 4, 4, “品牌ID”),newExcelHeaderInfo(1, 1, 5, 5, “品牌名称”),newExcelHeaderInfo(0, 0, 6, 7, “商店”),newExcelHeaderInfo(1, 1, 6, 6, “商店ID”),newExcelHeaderInfo(1, 1, 7, 7, “商店名称”),newExcelHeaderInfo(1, 1, 8, 8, “价格”),newExcelHeaderInfo(1, 1, 9, 9, “库存”),newExcelHeaderInfo(1, 1, 10, 10, “销量”),newExcelHeaderInfo(1, 1, 11, 11, “插入时间”),newExcelHeaderInfo(1, 1, 12, 12, “更新时间”),newExcelHeaderInfo(1, 1, 13, 13, “记录是否已经删除”));}// 获取格式化信息privateMap<String, ExcelFormat> getFormatInfo() {Map<String, ExcelFormat> format = newHashMap<>();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);returnformat;}
实现效果
源码分析
我们重点看ExcelUtils这个类,这个类是实现导出的核心,先来看一下三个成员变量。privateListlist;privateList<ExcelHeaderInfo> excelHeaderInfos;privateMap<String, ExcelFormat> formatInfo;
list
该成员变量用来保存待导出的数据。ExcelHeaderInfo
该成员变量主要用来保存表头信息,因为我们需要定义多个表头信息,所以需要使用一个列表来保存,ExcelHeaderInfo构造函数如下ExcelHeaderInfo(int firstRow, int lastRow, int firstCol, int lastCol, String title)firstRow:该表头所占位置的首行lastRow:该表头所占位置的尾行firstCol:该表头所占位置的首列lastCol:该表头所占位置的尾行title:该表头的名称ExcelFormat
该参数主要用来格式化字段,我们需要预先约定好转换成那种格式,不能随用户自己定。所以我们定义了一个枚举类型的变量,该枚举类只有一个字符串类型成员变量,用来保存想要转换的格式,例如FORMAT_INTEGER就是转换成整型。因为我们需要接受多个字段的转换格式,所以定义了一个Map类型来接收,该参数可以省略(默认格式为字符串)。
publicenumExcelFormat {FORMAT_INTEGER(“INTEGER”),FORMAT_DOUBLE(“DOUBLE”),FORMAT_PERCENT(“PERCENT”),FORMAT_DATE(“DATE”);privateString value;ExcelFormat(String value) {this.value= value;}publicString getValue() {returnvalue;}}
核心方法
1、创建表头
该方法用来初始化表头,而创建表头最关键的就是poi中Sheet类的addMergedRegion(CellRangeAddress var1)方法,该方法用于单元格融合。我们会遍历ExcelHeaderInfo列表,按照每个ExcelHeaderInfo的坐标信息进行单元格融合,然后在融合之后的每个单元首行和首列的位置创建单元格,然后为单元格赋值即可,通过上面的步骤就完成了任意类型的表头设置。// 创建表头privatevoidcreateHeader(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(newCellRangeAddress(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);}}2、转换数据
在进行正文赋值之前,我们先要对原始数据列表转换成字符串的二维数组,之所以转成字符串格式是因为可以统一的处理各种类型,之后有需要我们再转换回来即可。// 将原始数据转成二维数组privateString[][] transformData() {intdataSize = this.list.size();String[][] datas = newString[dataSize][];// 获取报表的列数Field[] fields = list.get(0).getClass().getDeclaredFields();// 获取实体类的字段名称数组List<String> columnNames = this.getBeanProperty(fields);for(inti = 0; i < dataSize; i ) {datas[i] = newString[fields.length];for(intj = 0; j < fields.length; j ) {try{// 赋值datas[i][j] = BeanUtils.getProperty(list.get(i), columnNames.get(j));} catch(Exception e) {LOGGER.error(“获取对象属性值失败”);e.printStackTrace();}}}returndatas;}
这个方法中我们通过使用反射技术,很巧妙的实现了任意类型的数据导出(这里的任意类型指的是任意的报表类型,不同的报表,导出的数据肯定是不一样的,那么在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后可以进行方便的运算)。 // 创建正文privatevoidcreateContent(Row row, CellStyle style, String[][] content, inti, Field[] fields) {List<String> columnNames = getBeanProperty(fields);for(intj = 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]);}}}
导出工具类的核心方法就差不多说完了,下面说一下关于多线程查询的问题。多扯两点
1. 多线程查询数据
理想很丰满,现实还是有点骨感的。LZ虽然对50w的数据分别创建20个线程去查询,但是总体的效率并不是50w/20,而是仅仅快了几秒钟,知道原因的小伙伴可以给我留个言一起探讨一下。下面先说说具体思路:因为多个线程之间是同时执行的,你不能够保证哪个线程先执行完毕,但是我们却得保证数据顺序的一致性。在这里我们使用了Callable接口,通过实现Callable接口的线程可以拥有返回值,我们获取到所有子线程的查询结果,然后合并到一个结果集中即可。那么如何保证合并的顺序呢?我们先创建了一个FutureTask类型的List,该FutureTask的类型就是返回的结果集。List<FutureTask<List<TtlProductInfoPo>>> tasks = newArrayList<>();
当我们每启动一个线程的时候,就将该线程的FutureTask添加到tasks列表中,这样tasks列表中的元素顺序就是我们启动线程的顺序。FutureTask<List<TtlProductInfoPo>> task = newFutureTask<>(newlistThread(map));log.info(“开始查询第{}条开始的{}条记录”, i * THREAD_MAX_ROW, THREAD_MAX_ROW);newThread(task).start();// 将任务添加到tasks列表中tasks.add(task);
接下来,就是顺序塞值了,我们按顺序从tasks列表中取出FutureTask,然后执行FutureTask的get()方法,该方法会阻塞调用它的线程,知道拿到返回结果。这样一套循环下来,就完成了所有数据的按顺序存储。for(FutureTask<List<TtlProductInfoPo>> task : tasks) {try{productInfoPos.addAll(task.get());} catch(Exceptione) {e.printStackTrace();}}
2. 如何解决接口超时
CREATETABLE`ttl_product_info`(`id`int(11) NOTNULLAUTO_INCREMENT COMMENT’记录唯一标识’,`product_name`varchar(50) NOTNULLCOMMENT’商品名称’,`category_id`bigint(20) NOTNULLDEFAULT’0’COMMENT’类型ID’,`category_name`varchar(50) NOTNULLCOMMENT’冗余分类名称-避免跨表join’,`branch_id`bigint(20) NOTNULLCOMMENT’品牌ID’,`branch_name`varchar(50) NOTNULLCOMMENT’冗余品牌名称-避免跨表join’,`shop_id`bigint(20) NOTNULLCOMMENT’商品ID’,`shop_name`varchar(50) NOTNULLCOMMENT’冗余商店名称-避免跨表join’,`price`decimal(10,2) NOTNULLCOMMENT’商品当前价格-属于热点数据,而且价格变化需要记录,需要价格详情表’,`stock`int(11) NOTNULLCOMMENT’库存-热点数据’,`sales_num`int(11) NOTNULLCOMMENT’销量’,`create_time`timestampNOTNULLDEFAULTCURRENT_TIMESTAMPCOMMENT’插入时间’,`update_time`timestampNOTNULLDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMPCOMMENT’更新时间’,`is_del`tinyint(3) unsignedNOTNULLDEFAULT’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=InnoDBAUTO_INCREMENT=15000001DEFAULTCHARSET=utf8 COMMENT=’商品信息表’;
?? 面试
最新整理JAVA面试题附答案
面试题:Java 集合高频要点问题你能答上来吗?
十个经典Java 集合面试题!
Redis 面试题,面试官能问的都被我找到了!
Java面试题 | 框架篇
String 面试题!看完让你恍然大悟!
详细解析面试题:Java的值传递 !
【面试题】Java 多线程与并发高频面试题解析
最全91道MySQL面试题 | 附答案解析