记录一次生产环境因POI导入无响应问题

问题描述

某天早晨售后人员反馈系统响应缓慢,且收到阿里云云监控短信。查看监控信息发现三台相互负载的服务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,解决超过几万行而导致内存溢出的问题 向大佬学习!