问题描述
某天早晨售后人员反馈系统响应缓慢,且收到阿里云云监控短信。查看监控信息发现三台相互负载的服务cpu均处于100%。
排查经过
-
查看log发现"java.lang.OutOfMemoryError: GC overhead limit exceeded"异常信息。
-
使用jstack导出堆栈信息发现存在一处RUNNABLE线程的导入信息。
-
初步判断为客户在使用导入功能,因为之前也出现过导入功能导致cpu增加。
-
向售后沟通是否有企业询问导入问题。一问还真有。
-
于是向客户索要excel文件。客户的文件出现以下问题。
-
不知道客户如何操作,导致excel出现空白单元格,由于过多的空白单元格导致POI占用过多内存,导致服务一直full gc。
-
将文件导入本地后发现代码运行到"new XSSFWorkbook(is);"时就没有响应。
-
查阅资料了解到POI默认的用户模式很容易内存溢出。
-
目前我们的方案为对模板行数、列数进行校验若超过限制的数量将提示联系我们。之前的代码行数、列数都是"new XSSFWorkbook(is);"之后才能获得到的
,于是采用"SAX事件驱动模式",通过继承DefaultHandler类,重写process(),startElement(),characters(),endElement()这四个方法。process()方式主要是遍历所有的sheet,并依次调用startElement()、characters()方法、endElement()这三个方法。只统计行数、列数对数据内容等其它方面均不处理。
package xxxxxxxxxxxxx;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.hssf.eventusermodel.*;
import org.apache.poi.hssf.eventusermodel.dummyrecord.LastCellOfRowDummyRecord;
import org.apache.poi.hssf.eventusermodel.dummyrecord.MissingCellDummyRecord;
import org.apache.poi.hssf.record.*;
import org.apache.poi.poifs.filesystem.POIFSFileSystem;
import java.io.FileInputStream;
/**
* @author y
* @create 2018-01-19 14:18
* @desc 用于解决.xls2003版本大数据量问题
**/
@Slf4j
public class ExcelXlsReader implements HSSFListener {
private Integer maxRowNumber = 0;
private Integer maxColNumber = 0;
/**
* 总行数
*/
private int totalRows = 0;
private int cellNum = 0;
/**
* 遍历excel下所有的sheet
*
* @param fileName
* @throws Exception
*/
public boolean process(String fileName) throws Exception {
MissingRecordAwareHSSFListener listener = new MissingRecordAwareHSSFListener(this);
FormatTrackingHSSFListener formatListener = new FormatTrackingHSSFListener(listener);
HSSFEventFactory factory = new HSSFEventFactory();
HSSFRequest request = new HSSFRequest();
EventWorkbookBuilder.SheetRecordCollectingListener workbookBuildingListener = new EventWorkbookBuilder.SheetRecordCollectingListener(formatListener);
request.addListenerForAllRecords(workbookBuildingListener);
factory.processWorkbookEvents(request, new POIFSFileSystem(new FileInputStream(fileName)));
log.error("return maxRowNumber: " + maxRowNumber + ", maxColNumber: " + maxColNumber);
return maxColNumber < 100 && 20000 > maxRowNumber;
}
/**
* HSSFListener 监听方法,处理Record
* 处理每个单元格
*
* @param record
*/
@SuppressWarnings("unchecked")
public void processRecord(Record record) {
switch (record.getSid()) {
case BoundSheetRecord.sid:
break;
case BOFRecord.sid: //开始处理每个sheet
BOFRecord br = (BOFRecord) record;
if (br.getType() == BOFRecord.TYPE_WORKSHEET) {
maxRowNumber = Math.max(maxRowNumber, totalRows);
totalRows = 0;
}
break;
case SSTRecord.sid:
break;
case BlankRecord.sid: //单元格为空白
cellNum++;
break;
case BoolErrRecord.sid: //单元格为布尔类型
cellNum++;
break;
case FormulaRecord.sid://单元格为公式类型
cellNum++;
break;
case StringRecord.sid: //单元格中公式的字符串
cellNum++;
break;
case LabelRecord.sid:
cellNum++;
break;
case LabelSSTRecord.sid: //单元格为字符串类型
cellNum++;
break;
case NumberRecord.sid: //单元格为数字类型
cellNum++;
break;
default:
break;
}
//空值的操作
if (record instanceof MissingCellDummyRecord) {
cellNum++;
}
//行结束时的操作
if (record instanceof LastCellOfRowDummyRecord) {
totalRows++;
maxRowNumber = Math.max(maxRowNumber, totalRows);
maxColNumber = Math.max(maxColNumber, cellNum);
cellNum = 0;
}
}
}
package xxxxxxx;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.openxml4j.opc.OPCPackage;
import org.apache.poi.xssf.eventusermodel.XSSFReader;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;
import org.xml.sax.helpers.XMLReaderFactory;
import java.io.InputStream;
@Slf4j
public class ExcelXlsxReaderWithDefaultHandler extends DefaultHandler {
/**
* 总行数
*/
private int totalRows=0;
/**
* 当前列
*/
private int curCol = 0;
private Integer maxRowNumber = 0;
private Integer maxColNumber = 0;
/**
* 遍历工作簿中所有的电子表格
* 并缓存在mySheetList中
*
* @throws Exception
*/
public boolean process(InputStream inputStream) throws Exception {
OPCPackage pkg = OPCPackage.open(inputStream);
XSSFReader xssfReader = new XSSFReader(pkg);
XMLReader parser = XMLReaderFactory.createXMLReader("org.apache.xerces.parsers.SAXParser");
parser.setContentHandler(this);
XSSFReader.SheetIterator sheets = (XSSFReader.SheetIterator) xssfReader.getSheetsData();
while (sheets.hasNext()) { //遍历sheet
totalRows = 0;
InputStream sheet = sheets.next(); //sheets.next()和sheets.getSheetName()不能换位置,否则sheetName报错
InputSource sheetSource = new InputSource(sheet);
parser.parse(sheetSource); //解析excel的每条记录,在这个过程中startElement()、characters()、endElement()这三个函数会依次执行
sheet.close();
}
log.error("return maxRowNumber: " + maxRowNumber + ", maxColNumber: " + maxColNumber);
return maxColNumber < 100 && 20000 > maxRowNumber;
}
/**
* 第一个执行
*
* @param uri
* @param localName
* @param name
* @param attributes
* @throws SAXException
*/
@Override
public void startElement(String uri, String localName, String name, Attributes attributes) throws SAXException {
}
/**
* 第二个执行
* 得到单元格对应的索引值或是内容值
* 如果单元格类型是字符串、INLINESTR、数字、日期,lastIndex则是索引值
* 如果单元格类型是布尔值、错误、公式,lastIndex则是内容值
* @param ch
* @param start
* @param length
* @throws SAXException
*/
@Override
public void characters(char[] ch, int start, int length) throws SAXException {
}
/**
* 第三个执行
*
* @param uri
* @param localName
* @param name
* @throws SAXException
*/
@Override
public void endElement(String uri, String localName, String name) throws SAXException {
if ("c".equals(name)) {
curCol++;
}
//如果标签名称为row,这说明已到行尾,调用optRows()方法
if ("row".equals(name)) {
totalRows++;
maxRowNumber = Math.max(maxRowNumber, totalRows);
maxColNumber = Math.max(maxColNumber, curCol);
curCol = 0;
}
}
}
public static boolean checkExcel(MultipartFile file) throws Exception {
boolean checkExcel;
String fileName = file.getOriginalFilename();
//处理excel2003文件
if (fileName.toLowerCase().endsWith(EXCEL03_EXTENSION)) {
ExcelXlsReader excelXls=new ExcelXlsReader();
checkExcel =excelXls.process(fileName);
} else if (fileName.toLowerCase().endsWith(EXCEL07_EXTENSION)) {
//处理excel2007文件
ExcelXlsxReaderWithDefaultHandler excelXlsxReader = new ExcelXlsxReaderWithDefaultHandler();
try(InputStream inputStream = file.getInputStream();){
checkExcel = excelXlsxReader.process(inputStream);
} catch (IOException e) {
e.printStackTrace();
log.error("checkExcel: ", e);
checkExcel = false;
}
} else {
throw new Exception("文件格式错误,fileName的扩展名只能是xls或xlsx。");
}
return checkExcel;
}
总结:因为业务允许,所以自定义了获取poi行数、列数方法,对其进行校验,最终防止类似文件导致内存溢出。
另外我们对用户导入行为添加了日志,可以快速知道哪个客户导入了什么文件,若这次不是客户主动联系售后,
要找到根本原因将非常困难。
本文主要参考 牧梦者-POI读写大数据量excel,解决超过几万行而导致内存溢出的问题 向大佬学习!