POI导出40万数据量(POI大数据量导出),解决SXSSFWorkbook列宽自适应问题及poi导出OutOfMemoryError: GC overhead limit exceeded问题

1.HSSFWorkbook、XSSFWorkbook、SXSSFWorkbook的区别:

 ◎HSSFWorkbook一般用于Excel2003版及更早版本(扩展名为.xls)的导出,单Sheet导出条数上限是65535行,256列

 ◎XSSFWorkbook一般用于Excel2007版(扩展名为.xlsx)的导出。单Sheet导出条数1048576行,16384列,
 但是实测30万40万导出是,会报错Handler dispatch failed; nested exception is java.lang.OutOfMemoryError: GC overhead limit exceeded
 正常导出的话,40万数据纯导出大概在1分钟左右

 ◎SXSSFWorkbook一般用于大数据量的导出。
 40万数据完美导出,总耗时1.8分钟

2.SXSSFWorkbook导出代码:

2.1导出代码(解决自适应列宽问题)

     public void exportSheetSXSS(String fileName, String sheetName, String title, LinkedHashMap<String, String> keyNameMap, List<HashMap<String, Object>> dataList, int rowRowNum, HttpServletRequest request, HttpServletResponse response) {
        SXSSFWorkbook workbook = null;
        try {
            //设置-1表示不限制大小,否则后面shee.getRow时一直为null
            workbook = new SXSSFWorkbook();                        // 创建工作簿对象
            // 打开压缩功能 防止占用过多磁盘
            workbook.setCompressTempFiles(true);
            //设置标题和单元格样式
            CellStyle columnTopStyle = IExclUtil.getColumnTopStyleXSS(workbook);  //获取列头样式对象
            CellStyle style = IExclUtil.getStyleXSS(workbook);                    //单元格样式对象
            int columnNum = keyNameMap.size();  // 定义所需列数
            SXSSFSheet sheet = workbook.createSheet(sheetName);// 创建工作表
          //  sheet.setRandomAccessWindowSize(dataList.size()+10);//windowSize为-1表示无限制访问。在这种情况下,所有尚未通过调用flushRows()刷新的记录可用于随机访问。
            //设置报表title
            setTitleSXSS(sheet, columnTopStyle, columnNum, title, mergeRowsList);
            //设置标题信息
            Map<Integer,Integer> sizeMap = setHeadColumSXSS(sheet, columnTopStyle, rowRowNum, keyNameMap);
            //将查询出的数据设置到sheet对应的单元格中,并自适应长度
            initDataSXSSAndAutoExpandColumWidth(sheet, style, rowRowNum, dataList, keyNameMap,sizeMap);
            //导出数据
            exportSXSS(fileName, workbook, response, request);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (workbook != null) {
                //使用完毕后将产生的临时文件删除 防止将磁盘搞满
                workbook.dispose();
            }
        }
    }

setRandomAccessWindowSize(-1)设置后,基本上就导不出来了
2.2设置报表title

    default void setTitleSXSS(SXSSFSheet sheet, CellStyle columnTopStyle, int columnNum, String tileName, LinkedList<int[]> mergeRowsList) {
        // 产生表格标题行
        SXSSFRow rowm = sheet.createRow(0);
        SXSSFCell cellTiltle = rowm.createCell(0);
        //合并单元格
        sheet.addMergedRegion(new CellRangeAddress(0, 1, 0, columnNum - 1));
        if (!CollectionUtils.isEmpty(mergeRowsList)) {
            for (int[] a : mergeRowsList) {
                sheet.addMergedRegion(new CellRangeAddress(a[0], a[1], a[2], a[3]));
            }
        }
        cellTiltle.setCellStyle(columnTopStyle);
        cellTiltle.setCellValue(tileName);
    }

2.3设置报表列名,返回所有列名的索引及对应的列名长度

    /**
     * 设置列名,返回所有列名的索引及对应的列名长度
     * @param sheet
     * @param columnTopStyle
     * @param rowRowNum
     * @param keyNameMap
     * @return 所有列名的索引及对应的列名长度,key是索引,value是长度(一定要用索引做key)
     */
    default Map<Integer,Integer> setHeadColumSXSS(SXSSFSheet sheet, CellStyle columnTopStyle, int rowRowNum, LinkedHashMap<String, String> keyNameMap) {
        // 将列头设置到sheet的单元格中
        SXSSFRow rowRowName = sheet.createRow(rowRowNum);
        int sizeIndex = 0;
        Map<Integer,Integer> sizeMap = new HashMap<>();
        for (Map.Entry<String, String> entry : keyNameMap.entrySet()) {
            SXSSFCell cellRowName = rowRowName.createCell(sizeIndex);//创建列头对应个数的单元格
            cellRowName.setCellType(HSSFCell.CELL_TYPE_STRING);//设置列头单元格的数据类型
            String value = entry.getValue();
            cellRowName.setCellValue(value);//设置列头单元格的值
            cellRowName.setCellStyle(columnTopStyle);
            sizeMap.put(sizeIndex, Math.max(0, value.getBytes().length));
            ++sizeIndex;
        }
        return sizeMap;
    }

2.4将查询出的数据设置到sheet对应的单元格中,并自适应长度

  /**
     * 将查询出的数据设置到sheet对应的单元格中,并自适应长度
     * @param sheet
     * @param style
     * @param rowRowNum
     * @param dataList
     * @param keyNameMap
     * @param sizeMap 长度map
     */
    default void initDataSXSSAndAutoExpandColumWidth(SXSSFSheet sheet, CellStyle style, int rowRowNum, List<HashMap<String, Object>> dataList, LinkedHashMap<String, String> keyNameMap,Map<Integer,Integer> sizeMap) {
        if (dataList == null) {
            SXSSFRow row = sheet.createRow(rowRowNum + 1);//创建所需的行数
            SXSSFCell cell = row.createCell(0);
            cell.setCellValue("");
            cell.setCellStyle(style);
        } else {
            for (int i = 0; i < dataList.size(); i++) {
                HashMap<String, Object> obj = dataList.get(i);//遍历每个对象
                SXSSFRow row = sheet.createRow(i + rowRowNum + 1);//创建所需的行数
                int cellIndex = 0;
                for (Map.Entry<String, String> entry : keyNameMap.entrySet()) {
                    SXSSFCell cell = null;
                    cell = row.createCell(cellIndex);
                    String key = entry.getKey();
                    Object o = obj.get(key);
                    if(o!=null){
                        String value = o.toString();
                        cell.setCellValue(value);
                        sizeMap.put(cellIndex,Math.max(sizeMap.get(cellIndex), value.getBytes().length));
                    }else{
                        cell.setCellValue("");
                        sizeMap.put(cellIndex,Math.max(sizeMap.get(cellIndex), 0));
                    }
                    cell.setCellStyle(style);
                    ++cellIndex;
                }
            }
            //列的索引做长度集合的key
            for (Integer cellIndex : sizeMap.keySet()) {
                int width =Math.min(65280,(sizeMap.get(cellIndex)+2) * 256);
                sheet.setColumnWidth(cellIndex, width);
            }
        }
    }

2.5导出

    default void exportSXSS(String fileName, SXSSFWorkbook workbook, HttpServletResponse response, HttpServletRequest request) {
        try (OutputStream ouputStream = response.getOutputStream()) {
            response.setCharacterEncoding("UTF-8");
            response.setContentType("multipart/form-data");
            String userAgent = request.getHeader("User-Agent");
            byte[] bytes = userAgent.contains("MSIE") ? fileName.getBytes() : fileName.getBytes("UTF-8"); // fileName.getBytes("UTF-8")处理safari的乱码问题
            String fileNameEncode = new String(bytes, "ISO-8859-1"); // 各浏览器基本都支持ISO编码
            response.setHeader("Content-disposition",
                    String.format("attachment; filename=\"%s\"", fileNameEncode + ".xlsx"));
            workbook.write(ouputStream);
            ouputStream.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

2.6 调用

   @GetMapping("/download")
    public void download(
            HttpServletRequest request, HttpServletResponse response, 
            @RequestParam(value = "countFlag", required = false) String countFlag) {
        Result<?> result = pbService.selectAl(countFlag);
        List<HashMap<String, Object>> records = (List<HashMap<String, Object>>) result.getData();
        ExportExcelUtil exportExcelUtil = new ExportExcelUtil();
        exportExcelUtil.exportSheetSXSS("下载文件名","sheet名称","标题名称",keymap(),records, 2, request, response);
    }

3.踩坑记

3.1列宽自适应失效(列宽不准确)问题

在这里插入图片描述
修改之后
在这里插入图片描述
解决代码见2.3与2.4
(原代码及问题)

        //让列宽随着导出的列长自动适应
        //表头数组
        String[] headers = new String[keyNameMap.size()];
        int ii = 0;
        for (Iterator<String> iter = keyNameMap.keySet().iterator();iter.hasNext();) {
            String fieldName = iter.next();
            headers[ii] = fieldName;
            ii++;
        }
        for (int i = 0; i < headers.length; i++) {
            sheet.autoSizeColumn(i);
            int width = Math.max(3480, Math.min(65280, sheet.getColumnWidth(i) * 17 / 10));
            sheet.setColumnWidth(i, width);
        }
3.1.1 sheet.autoSizeColumn(i);sheet.setColumnWidth(i, width);无效

列宽自适应失效的原因有可能是因为标题部分用了单元格合并,使用autoSizeColumn(i)后,导致sheet.getColumnWidth(i)无法获取到列的宽度,只能获取到默认长度,自然自适应就失效了.

解决方式一(大数据量无法使用)

在设置自适应列宽时,使用次方法 sheet.autoSizeColumn(i,true),有合并单元格用此方法
例:

        for (int i = 0; i < headers.length; i++) {
            sheet.autoSizeColumn(i,true);
            int width = Math.max(3480, Math.min(65280, sheet.getColumnWidth(i) * 17 / 10));
            sheet.setColumnWidth(i, width);
        }

弊端: public void autoSizeColumn(int column, boolean useMergedCells)方法效率极差,大数据量不可用

解决方式二(效率差)

思路就是设置完值之后,在取出来比较长度,效率差

 default void autoExpandColumWidthMergeXSS(SXSSFSheet sheet, int columnNum,int rowNum) {
        for (int colNum = 0; colNum < columnNum; colNum++) {
            sheet.autoSizeColumn(colNum,true);
            int columnWidth = sheet.getColumnWidth(colNum) / 256;
            //rowNum是列名所在行
            for (int a=rowNum; a < sheet.getLastRowNum(); a++) {
                SXSSFRow currentRow = sheet.getRow(a);
                if (currentRow != null) {
                    if (currentRow.getCell(colNum) != null) {
                        //取得当前的单元格
                        SXSSFCell currentCell = currentRow.getCell(colNum);
                        //如果当前单元格类型为字符串
                        int length = getCellValue(currentCell).getBytes().length;
                        if (columnWidth < length) {
                            //将单元格里面值大小作为列宽度
                            columnWidth = length;
                        }
                    }
                }
            }
            int i = (columnWidth + 2) * 256;
            if (i < 65280) {
                sheet.setColumnWidth(colNum, i);
            } else {
                sheet.setColumnWidth(colNum, 6000);
            }
        }
    }
     /**
     * Description: 获取单元格的值
     *
     * @param cell
     * @return 单元格中的值
     */
    public static String getCellValue(Cell cell) {
        String value = "";
        if (cell == null) {
            return null;
        }
        switch (cell.getCellType()) {
            case Cell.CELL_TYPE_NUMERIC:
                value = cell.getNumericCellValue() + "";
                break;
            case Cell.CELL_TYPE_STRING:
                value = cell.getRichStringCellValue() + "";
                break;
            case Cell.CELL_TYPE_FORMULA:
                value = cell.getCellFormula();
                break;
            case Cell.CELL_TYPE_BOOLEAN:
                value = cell.getBooleanCellValue() + "";
                break;
            default:
                break;
        }
        return value.trim();
    }
解决方式三(完美解决) 代码在上面

思路:1.设置列名的时候,就把列索引及列名长度存到map里(取名sizeMap),key为列在excel里的索引,value为当前单元格字段长度
2.设置内容的时候,把单元格内容存进去的同时,获取其长度与sizeMap里该列以存的长度做比较,取最大的存进去
3.最后在调用setColumnWidth方法,把sizeMap里存的列及长度设置进去,完美解决
40万数据量,原本总用时2.5min,优化后总用时1.8min

3.2报错Handler dispatch failed; nested exception is java.lang.OutOfMemoryError: GC overhead limit exceeded

1.调整jvm内存大小即可,本地复现方法为:加jvm参数即可,如下图
在这里插入图片描述2. 或者使添加vm参数-XX:+UseCompressedOops

4 结果

在这里插入图片描述
在这里插入图片描述

实测42万数据量,纯导出时间为35秒

参考链接:
POI SXSSF注意事项

5 SXSSFWorkbook windows下临时文件存放路径

C:\Users\你的用户\AppData\Local\Temp\poifiles
(AppData)为隐藏文件夹
能在此文件夹下看到临时文件
在这里插入图片描述

  //使用完毕后将产生的临时文件删除 防止将磁盘搞满
                workbook.dispose();
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值