zoukankan      html  css  js  c++  java
  • 记录一次生产环境因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,解决超过几万行而导致内存溢出的问题 向大佬学习!

  • 相关阅读:
    ado.net
    sql基础
    css样式
    HTML基础加强
    socket网络编程
    网络聊天室
    多线程
    WinForm基础
    使用Maven插件(plugin)MyBatis Generator逆向工程
    SpringBoot使用thymeleaf时候遇到无法渲染问题(404/500)
  • 原文地址:https://www.cnblogs.com/chwwww/p/14291956.html
Copyright © 2011-2022 走看看