Java easy poi 模板 导出 内存溢出 OutOfMemoryError

Java easy poi 模板 导出 内存溢出 OutOfMemoryError

    • 异常描述
    • 使用的jar包版本
    • 错误症状
    • 解决方案(底部有详细解决方案)
    • 错误跟踪
    • 异常梳理
    • 异常处理
        • 处理补充
    • 错误总结

异常描述

在使用easy poi 模板导出时出现了 OutOfMemoryError 异常

使用的jar包版本

org.jeecg.easypoi-base.2.3.1
org.jeecg.easypoi-web.2.3.1

错误症状

我的模板中运用了多个list 列表导出,在数据条数到达某个值是就会出现该内存溢出,所谓的某个值,就是当我第一个list 列表是两条 或者三条记录是没有问题当有四条记录时就会报错,五条又会变好,类似就是在条数符合某个值是就会出现该异常

解决方案(底部有详细解决方案)

1.将cell验证位置减到原模板cell的位置再去验证查询是否有合并单元格
2.每次进行完插入行操作后重新加载一遍sheet的合并单元格位置
3.处理导出模板

错误跟踪

报错信息显示该错误出现位置在easy poi 中的JeecgTemplateExcelView类中,查看源码此处只是将只拆分后调用其他方法
A.

@Controller("jeecgTemplateExcelView")
public class JeecgTemplateExcelView extends MiniAbstractExcelView {
    public JeecgTemplateExcelView() {
    }

    protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
        String codedFileName = "临时文件";
        //这行 只是将model拆分后 调用了其工具方法进行excel导出
        Workbook workbook = ExcelExportUtil.exportExcel((TemplateExportParams)model.get("params"), (Class)model.get("entity"), (List)model.get("list"), (Map)model.get("map"));
        if (model.containsKey("fileName")) {
            codedFileName = (String)model.get("fileName");
        }

        if (workbook instanceof HSSFWorkbook) {
            codedFileName = codedFileName + ".xls";
        } else {
            codedFileName = codedFileName + ".xlsx";
        }

        if (this.isIE(request)) {
            codedFileName = URLEncoder.encode(codedFileName, "UTF8");
        } else {
            codedFileName = new String(codedFileName.getBytes("UTF-8"), "ISO-8859-1");
        }

        response.setHeader("content-disposition", "attachment;filename=" + codedFileName);
        ServletOutputStream out = response.getOutputStream();
        workbook.write(out);
        out.flush();
    }
}

方法进去后其有调用了ExcelExportOfTemplateUtil 的createExcleByTemplate 方法,根据方法意思理解就是根据模板创建一个excel 文件,
B.

public Workbook createExcleByTemplate(TemplateExportParams params, Class<?> pojoClass, Collection<?> dataSet, Map<String, Object> map) {
        if (params != null && map != null && !StringUtils.isEmpty(params.getTemplateUrl())) {
            Workbook wb = null;

            try {
				//将参数赋值给该列中变量,该对象存放的一些对excel的设置信息比如样式
                this.teplateParams = params;
				//获取当前excle  workBook 指的是一个Excel 
                wb = this.getCloneWorkBook();
				设置样式到当前excel中
                this.setExcelExportStyler((IExcelExportStyler)this.teplateParams.getStyle().getConstructor(Workbook.class).newInstance(wb));
                int i = 0;

				遍历工作表(sheet)插入到新的excel中,并对每一个工作表(sheet)进行模板扫描转换和赋值
                for(int le = params.isScanAllsheet() ? wb.getNumberOfSheets() : params.getSheetNum().length; i < le; ++i) {
                    if (params.getSheetName() != null && params.getSheetName().length > i && StringUtils.isNotEmpty(params.getSheetName()[i])) {
                        wb.setSheetName(i, params.getSheetName()[i]);
                    }

                    this.tempCreateCellSet.clear();
					//这个方法就是值得我们关注的方法,解析模板
                    this.parseTemplate(wb.getSheetAt(i), map, params.isColForEach());
                }

                if (dataSet != null) {
                    this.dataHanlder = params.getDataHanlder();
                    if (this.dataHanlder != null) {
                        this.needHanlderList = Arrays.asList(this.dataHanlder.getNeedHandlerFields());
                    }

                    this.addDataToSheet(pojoClass, dataSet, wb.getSheetAt(params.getDataSheetNum()), wb);
                }

                return wb;
            } catch (Exception var8) {
                LOGGER.error(var8.getMessage(), var8);
                return null;
            }
        } else {
            throw new ExcelExportException(ExcelExportEnum.PARAMETER_ERROR);
        }
    }

调用 parseTemplate();对导入的模板进行解析,
C.

//该方法我们可以看到将sheet 遍历出每一行,并将每一行中的每个单元格进行遍历和处理
private void parseTemplate(Sheet sheet, Map<String, Object> map, boolean colForeach) throws Exception {
        this.deleteCell(sheet, map);
        //初始化sheet 所有的合并单元格,这个需要注意这是引起错误的一部分原因
        this.mergedRegionHelper = new MergedRegionHelper(sheet);
        this.templateSumHanlder = new TemplateSumHanlder(sheet);
        if (colForeach) {
            this.colForeach(sheet, map);
        }

        Row row = null;
        int index = 0;

        while(true) {
            do {
                if (index > sheet.getLastRowNum()) {
                    this.hanlderSumCell(sheet);
                    return;
                }

                row = sheet.getRow(index++);
            } while(row == null);

            for(int i = row.getFirstCellNum(); i < row.getLastCellNum(); ++i) {
                if (row.getCell(i) != null && !this.tempCreateCellSet.contains(row.getRowNum() + "_" + row.getCell(i).getColumnIndex())) {
                //为每一个单元格设置值,值从map中获取
                    this.setValueForCellByMap(row.getCell(i), map);
                }
            }
        }
    }

解析出来的cell 将其内容进行替换
D.

private void setValueForCellByMap(Cell cell, Map<String, Object> map) throws Exception {
        int cellType = cell.getCellType();
        if (cellType == 1 || cellType == 0) {
            cell.setCellType(1);
            String oldString = cell.getStringCellValue();
			//过滤掉list 遍历,并将其模板解析后将值放入该单元格
            if (oldString != null && oldString.indexOf("{{") != -1 && !oldString.contains("fe:")) {
                String params = null;
                boolean isNumber = false;
                if (this.isNumber(oldString)) {
                    isNumber = true;
                    oldString = oldString.replaceFirst("n:", "");
                }

                while(oldString.indexOf("{{") != -1) {
                    params = oldString.substring(oldString.indexOf("{{") + 2, oldString.indexOf("}}"));
                    oldString = oldString.replace("{{" + params + "}}", PoiElUtil.eval(params, map).toString());
                }

                if (isNumber && StringUtils.isNotBlank(oldString)) {
                    cell.setCellValue(Double.parseDouble(oldString));
                    cell.setCellType(0);
                } else {
                    cell.setCellValue(oldString);
                }
            }
			//单独处理插入list 数据的地方
            if (oldString != null && oldString.contains("fe:")) {
                this.addListDataToExcel(cell, map, oldString.trim());
            }

        }
    }

上述锁定问题出现在多列表导出时所以我们着重观察addListDataToExcel 方法
E.

//添加list数据到Excel中,该方法并不是只处理一个单元格cell 的 数据而是将整个list 的数据处理完,也就是多行多单元格的插入
private void addListDataToExcel(Cell cell, Map<String, Object> map, String name) throws Exception {
		//判断是否需要创建新的行
        boolean isCreate = !name.contains("!fe:");
		//判断是否需要将原来位置数据向下移动list.size
        boolean isShift = name.contains("$fe:");
		//获取key ,通过该key 去向map中获取value
        name = name.replace("!fe:", "").replace("$fe:", "").replace("fe:", "").replace("{{", "");
        String[] keys = name.replaceAll("\\s{1,}", " ").trim().split(" ");
		//从map 中获取value数据集合
        Collection<?> datas = (Collection)PoiPublicUtil.getParamsValue(keys[0], map);
		//这一行就是最终的报错方法,通过方法名可知这个是获取list列表中的每列的key,也就是列名 根据这个去获取值
        Object[] columnsInfo = this.getAllDataColumns(cell, name.replace(keys[0], ""), this.mergedRegionHelper);
        if (datas != null) {
            Iterator<?> its = datas.iterator();
            int rowspan = (Integer)columnsInfo[0];
            int colspan = (Integer)columnsInfo[1];
            List<ExcelForEachParams> columns = (List)columnsInfo[2];
            Row row = null;
            int rowIndex = cell.getRow().getRowNum() + 1;
            Object t;
            if (its.hasNext()) {
                t = its.next();
                this.setForEeachRowCellValue(isCreate, cell.getRow(), cell.getColumnIndex(), t, columns, map, rowspan, colspan, this.mergedRegionHelper);
                rowIndex += rowspan - 1;
            }
			//判断是否需要将原来位置数据向下移动list.size,相当于excle的插入行操作
			//注意这里!!!!!!!!!!!
			//标记:insertRow
            if (isShift && datas.size() * rowspan > 1) {
                cell.getRow().getSheet().shiftRows(cell.getRowIndex() + rowspan, cell.getRow().getSheet().getLastRowNum(), (datas.size() - 1) * rowspan, true, true);
                this.templateSumHanlder.shiftRows(cell.getRowIndex(), (datas.size() - 1) * rowspan);
            }
			//遍历设置值
            while(its.hasNext()) {
                t = its.next();
                row = this.createRow(rowIndex, cell.getSheet(), isCreate, rowspan);
                this.setForEeachRowCellValue(isCreate, row, cell.getColumnIndex(), t, columns, map, rowspan, colspan, this.mergedRegionHelper);
                rowIndex += rowspan;
            }

        }
    }

getAllDataColumns
F.

//获取所有数据列,最终导致内存溢出的就是该方法中的while 循环,cell 一直为空,但是在模板中我已经写了}}结束符,
//由于一些原因他跳过了结束符那个Cell 执行到了下一个,导致往后的Cell 都是空,所以会一直向columns中添加对象直至内存溢出
//导致错误的根本原因是 mergedRegionHelper 这个对象,这个对象中存放的是整个Sheet合并单元格的位置
private Object[] getAllDataColumns(Cell cell, String name, MergedRegionHelper mergedRegionHelper) {
        List<ExcelForEachParams> columns = new ArrayList();
        cell.setCellValue("");
		//获取该单元格的位置及key
        columns.add(this.getExcelTemplateParams(name.replace("}}", ""), cell, mergedRegionHelper));
        int rowspan = 1;
        int colspan = 1;
        int index;
        if (!name.contains("}}")) {
            index = cell.getColumnIndex();
            int startIndex = cell.getColumnIndex();
            Row row = cell.getRow();

			//遍历获取该行下面单元格的位置及key
            label70:
            while(true) {
                while(true) {
				//判断该上一个单元格的是否合并单元格,如果合并,就从合并位置之后开始,如果未合并就从下一个开始,
				//由于此处导致检测到有合并的单元格导致循环直接跳过结束符号,没有了结束条件,导致死循环
                    int colSpan = columns.get(columns.size() - 1) != null ? ((ExcelForEachParams)columns.get(columns.size() - 1)).getColspan() : 1;
                    index += colSpan;

                    for(int i = 1; i < colSpan; ++i) {
                        columns.add((Object)null);
                    }

                    cell = row.getCell(index);
					
                    if (cell == null) {
                        columns.add((Object)null);
                    } else {
                        String cellStringString;
                        try {
                            cellStringString = cell.getStringCellValue();
                            if (StringUtils.isBlank(cellStringString) && colspan + startIndex <= index) {
                                throw new ExcelExportException("for each 当中存在空字符串,请检查模板");
                            }

                            if (StringUtils.isBlank(cellStringString) && colspan + startIndex > index) {
                                columns.add(new ExcelForEachParams((String)null, cell.getCellStyle(), (short)0));
                                continue;
                            }
                        } catch (Exception var13) {
                            throw new ExcelExportException(ExcelExportEnum.TEMPLATE_ERROR, var13);
                        }

                        cell.setCellValue("");
                        if (cellStringString.contains("}}")) {
                            columns.add(this.getExcelTemplateParams(cellStringString.replace("}}", ""), cell, mergedRegionHelper));
                            break label70;
                        }
						//而导致检测到错误的单元格位置的方法就是getExcelTemplateParams 
                        if (cellStringString.contains("]]")) {
                            columns.add(this.getExcelTemplateParams(cellStringString.replace("]]", ""), cell, mergedRegionHelper));
                            colspan = index - startIndex + 1;
                            index = startIndex - 1;
                            row = row.getSheet().getRow(row.getRowNum() + 1);
                            ++rowspan;
                        } else {
                            columns.add(this.getExcelTemplateParams(cellStringString.replace("]]", ""), cell, mergedRegionHelper));
                        }
                    }
                }
            }
        }

        colspan = 0;

        for(index = 0; index < columns.size(); ++index) {
            colspan += columns.get(index) != null ? ((ExcelForEachParams)columns.get(index)).getColspan() : 0;
        }

        colspan /= rowspan;
        return new Object[]{rowspan, colspan, columns};
    }
	
	//获取excel模板中的参数
	private ExcelForEachParams getExcelTemplateParams(String name, Cell cell, MergedRegionHelper mergedRegionHelper) {
        name = name.trim();
		初始化单元格
        ExcelForEachParams params = new ExcelForEachParams(name, cell.getCellStyle(), cell.getRow().getHeight());
        if (name.startsWith("'") && name.endsWith("'")) {
            params.setName((String)null);
            params.setConstValue(name.substring(1, name.length() - 1));
        }

		//验空
        if ("&NULL&".equals(name)) {
            params.setName((String)null);
            params.setConstValue("");
        }
		//查看该单元格是否被合并,此处拿到了错误的合并信息导致循环时跳过了结束符,没有了结束条件,导致死循环
        if (mergedRegionHelper.isMergedRegion(cell.getRowIndex() + 1, cell.getColumnIndex())) {
            Integer[] colAndrow = mergedRegionHelper.getRowAndColSpan(cell.getRowIndex() + 1, cell.getColumnIndex());
            params.setRowspan(colAndrow[0]);
            params.setColspan(colAndrow[1]);
        }

        params.setNeedSum(this.templateSumHanlder.isSumKey(params.getName()));
        return params;
    }

异常梳理

1.综上《F》分析导致错误的原因就是因为在cell 判断是否有相关的合并单元格时判断错误,导致循环索引位置直接跨过了结束符“}}” 所以没有了结束循环的条件导致程序陷入死循环,由于每次循环都会向集合中添加一个对象所以最红导致内存溢出
2.单元格合并位置集合存放在 mergedRegionHelper 中,跟踪这个对象可看出这个对象是在《c》模块代码中的parseTemplate方法进行了初始化,然后再去执行下面根据模板填充,在这个初始化后合并单元格集合内容正确无误
3.既然2时的mergedRegionHelper对象没有问题,就需要接着往下的模块看《E》模块的:isShift 也就是说当使用{{$fe}}时会将这个变量变为true,从而执行sheet的 shiftRows 方法,这个方法相当于在开始位置插入list.size 行
4.已知在我的导出模板是多列表,由于在第一个列表填充完毕后它在模板上插入了几行也就是说这时的模板的部分位置已经和原始模板不一致了,这是的mergedRegionHelper记录的合并单元格位置信息已经不准确了,但是源码中并没有进行这个变量的更新
5.这就导致我们判断第二个列表的cell 是否有合并的单元格时用新模板的位置读取了老模板位置的合并位置,(原来第二个list 第一个位置是A14,G14;如果第一个list为2条记录,经过插入后产生了新的模板,那么第二list去验证时其实验证的是用A16,G16这个位置,如果恰巧你这个位置在老模板上也有合并单元格,那么他就读取的合并信息就是错误的))

6.问题原因找到了处理方案也就相应出来了,处理方案可分为两种
1.将cell验证位置减到原模板cell的位置再去验证查询是否有合并单元格
2.每次进行完插入行操作后重新加载一遍sheet的合并单元格位置
3.处理导出模板

异常处理

1.第二方案(每次进行完插入行操作后重新加载一遍sheet的合并单元格位置

这个处理方案我们只需要在《E》模板加一行代码就可以,将一下代码加到ExcelExportOfTemplateUtil.addListDataToExcel方法中 实现重新读取sheet 合并单元格位置信息
if (isShift && datas.size() * rowspan > 1) {
   cell.getRow().getSheet().shiftRows(cell.getRowIndex() + rowspan, cell.getRow().getSheet().getLastRowNum(), (datas.size() - 1) * rowspan, true, true);
    this.templateSumHanlder.shiftRows(cell.getRowIndex(), (datas.size() - 1) * rowspan);
     //这是加入的代码
    this.mergedRegionHelper = new MergedRegionHelper(cell.getSheet());
}
 this.mergedRegionHelper = new MergedRegionHelper(cell.getSheet());
这是修改后的《E》模块
 private void addListDataToExcel(Cell cell, Map<String, Object> map, String name) throws Exception {
        boolean isCreate = !name.contains("!fe:");
        boolean isShift = name.contains("$fe:");
        name = name.replace("!fe:", "").replace("$fe:", "").replace("fe:", "").replace("{{", "");
        String[] keys = name.replaceAll("\\s{1,}", " ").trim().split(" ");
        Collection<?> datas = (Collection)PoiPublicUtil.getParamsValue(keys[0], map);
        Object[] columnsInfo = this.getAllDataColumns(cell, name.replace(keys[0], ""), this.mergedRegionHelper);
        if (datas != null) {
            Iterator<?> its = datas.iterator();
            int rowspan = (Integer)columnsInfo[0];
            int colspan = (Integer)columnsInfo[1];
            List<ExcelForEachParams> columns = (List)columnsInfo[2];
            Row row = null;
            int rowIndex = cell.getRow().getRowNum() + 1;
            Object t;
            if (its.hasNext()) {
                t = its.next();
                this.setForEeachRowCellValue(isCreate, cell.getRow(), cell.getColumnIndex(), t, columns, map, rowspan, colspan, this.mergedRegionHelper);
                rowIndex += rowspan - 1;
            }

            if (isShift && datas.size() * rowspan > 1) {
                cell.getRow().getSheet().shiftRows(cell.getRowIndex() + rowspan, cell.getRow().getSheet().getLastRowNum(), (datas.size() - 1) * rowspan, true, true);
                this.templateSumHanlder.shiftRows(cell.getRowIndex(), (datas.size() - 1) * rowspan);
                //这里是添加的代码
                this.mergedRegionHelper = new MergedRegionHelper(cell.getSheet());
            }

            while(its.hasNext()) {
                t = its.next();
                row = this.createRow(rowIndex, cell.getSheet(), isCreate, rowspan);
                this.setForEeachRowCellValue(isCreate, row, cell.getColumnIndex(), t, columns, map, rowspan, colspan, this.mergedRegionHelper);
                rowIndex += rowspan;
            }
        }
    }

处理补充

我采用第二方案是因为是个老项目,很多地方再用这个jar包,如果更换jar 代价会打一些,而且其他模板导出并没有问题,因为他们使用的是第三方案中的第一条,但是我这是不知道大概行数,最后我的处理方案就是将相关ExcelExportOfTemplateUtil类的addListDataToExcel方法重写

这是继承后重写的相关类,名称去掉Excel或Gztx就是原来的类名

2.第三方案(处理导出模板

1.使用{{!fe}}或{{fe}}标签
2.如果只有两个list,将第二个list向下的所有行的样式设置与list样式一致

错误总结

这个错误主要是因为使用{{$fe}},导致使用插入行命令,从而导致模板发生变化,让记录合并单元格位置对象的集合,记录的位置与模板不一致所导致的,
如果使用{{!fe}或{{fe}}不会执行插入行命令此错误也就不会出现

你可能感兴趣的:(Java,日常错误总结)