一、传统形式上进行百万导入
依据后面咱们的形式解决百万导出,那么就要做百万次循环
//获取数据private static Object getValue(Cell cell) { Object value = null; switch (cell.getCellType()) { case STRING: //字符串类型 value = cell.getStringCellValue(); break; case BOOLEAN: //boolean类型 value = cell.getBooleanCellValue(); break; case NUMERIC: //数字类型(蕴含日期和一般数字) if(DateUtil.isCellDateFormatted(cell)) { value = cell.getDateCellValue(); }else{ value = cell.getNumericCellValue(); } break; case FORMULA: //公式类型 value = cell.getCellFormula(); break; default: break; } return value;}
//1.依据上传流信息创立工作簿Workbook workbook = WorkbookFactory.create(attachment.getInputStream());//2.获取第一个sheetSheet sheet = workbook.getSheetAt(0);List<User> users = new ArrayList<>();//3.从第二行开始获取数据for (int rowNum = 1; rowNum <sheet.getLastRowNum(); rowNum++) { //获取行对象 Row row = sheet.getRow(rowNum); //获取该行的所有列单元格数量 Object objs[] = new Object[row.getLastCellNum()]; //从第二列获取数据 for(int cellNum = 0; cellNum < row.getLastCellNum();cellNum++) { Cell cell = row.getCell(cellNum); objs[cellNum] = getValue(cell); } //依据每一列结构用户对象 User user = new User(objs); users.add(user);}//第一个参数:用户列表,第二个参数:部门编码userService.save(users);return Result.SUCCESS();
这种形式去执行百万数据的读取,那么咱们在Jvisualvm看看效率
观看如图所示,咱们发现会一直的占用内存,直到吃满跑出oom异样
那么为什么会这样呢?
二、传统形式上问题剖析
那么为什么会占用那么多内存呢?
加载并读取Excel时,是通过一次性的将所有数据加载到内存中再去解析每个单元格内容。
当Excel数据量较大时,因为不同的运行环境可能会造成内存不足甚至OOM异样
在ApachePoi 官网提供了对操作大数据量的导入导出的工具和解决办法,操作Excel2007应用XSSF对象,能够分为三种模式:
- 用户模式:用户模式有许多封装好的办法操作简略,但
创立太多的对象,十分耗内存
(之前应用的办法) - 事件模式:
基于SAX形式解析XML
,SAX全称Simple API for XML,它是一个接口,也是一个软件包。它是一种XML解析的代替办法,不同于DOM解析XML文档时把所有内容一次性加载到内存中的形式,它逐行扫描文档,一边扫描,一边解析
。 - SXSSF对象:是用来生成海量excel数据文件,
次要原理是借助长期存储空间生成excel
Apache POI官网提供有一张图片,形容了基于用户模式,事件模式,以及应用SXSSF三种形式操作Excel的个性以及CUP和内存占用状况
三、解决思路剖析
咱们刚刚进行问题剖析晓得当咱们加载并读取Excel时,是通过一次性的将所有数据加载到内存中再去解析每个单元格内容
当百万数据级别的Excel导入时,随着单元格的一直读取,内存中对象越来越多,直至内存溢出。
下面提到POI提供刚给excel大数据时读取有两种模式:
1.用户模式:应用系列封装好的API操作Excel,使得操作简略但占用内存
2.事件驱动:基于sax的读取形式,逐行扫描文档,一边扫描,一边解析
那么为什么应用事件驱动就能够解决这个问题呢?
事件驱动原理解析
事件驱动一般来说会有一个事件监听的主线程,而所有要解决的事件则须要注册意思指产生某件事情的时候,委托给事件处理器进行解决
比如说:有一个事件处理器当每个月发工资时准时贷款一半工资
咱们刚刚提到过SAX是基于事件驱动的一种xml解析计划
那么事件处理器在解析excel的时候是如何去应用的呢?
其实就是指定xml里的一些节点,在指定的节点解析后触发事件
依照这样的思路办法:
咱们在解析excel时每一行实现后进行触发事件,事件做的事件就是将解析的以后行的内存进行销毁
这样咱们进行百万数据处理的时候,则解决完一行就开释一行
然而也有毛病:因为咱们是解决完一行就开释一行,则用完就销毁了不可在用
四、应用事件驱动来优化传统形式
那么依据SAX事件驱动进行读取数据次要分几种步骤
1.设置POI的事件模式,指定应用事件驱动去解析EXCEL来做
- 依据Excel 获取文件流
- 依据文件流创立OPCPackage
- 创立XSSFReader对象
2.应用Sax进行解析
- 自定义Sheet处理器
- 创立Sax的XMlReader对象
- 设置Sheet的事件处理器
- 逐行读取
接下来咱们依据步骤思路,去实现百万数据的读取,首先咱们将excel对应的数据实体类创立
public class PoiEntity { private String id; private String name; private String tel; //省略get、set办法}
接着创立咱们的自定义Sheet基于Sax的解析处理器SheetHandler
/** * 自定义Sheet基于Sax的解析处理器 * 解决每一行数据读取 * 实现接口org.apache.poi.xssf.eventusermodel.XSSFSheetXMLHandler */public class SheetHandler implements XSSFSheetXMLHandler.SheetContentsHandler { //封装实体对象 private PoiEntity entity; /** * 解析行开始 */ @Override public void startRow(int rowNum) { if (rowNum >0 ) { entity = new PoiEntity(); } } /** * 解析每一个单元格 * cellReference 单元格名称 * formattedValue 单元格数据值 * comment 单元格批注 */ @Override public void cell(String cellReference, String formattedValue, XSSFComment comment){ if(entity != null) { //因为单元格名称比拟长,所以截取首字母 switch (cellReference.substring(0, 1)) { case "A": entity.setId(formattedValue); break; case "B": entity.setName(formattedValue); break; case "C": entity.setTel(formattedValue); break; default: break; } } } /** * 解析每一行完结时触发 */ public void endRow(int rowNum) { System.out.println(entity); //个别进行应用对象进行业务解决..... }}
接下来咱们应用事件模型来解析百万数据excel报表
//excel文档门路String path = "e:\\demo.xlsx";//============设置POI的事件模式,指定应用事件驱动去解析EXCEL来做============//1.依据Excel获取OPCPackage对象OPCPackage pkg = OPCPackage.open(path, PackageAccess.READ);//2.创立XSSFReader对象XSSFReader reader = new XSSFReader(pkg);//3.获取String类型表格SharedStringsTable对象SharedStringsTable sst = reader.getSharedStringsTable();//4.获取款式表格StylesTable对象StylesTable styles = reader.getStylesTable();//============应用Sax进行解析============//5.创立Sax的XmlReader对象XMLReader parser = XMLReaderFactory.createXMLReader();//6.设置Sheet的事件处理器parser.setContentHandler(new XSSFSheetXMLHandler(styles,sst,new SheetHandler(), false));//7.逐行读取(因为有多个sheet所以须要迭代读取)XSSFReader.SheetIterator sheets = (XSSFReader.SheetIterator)reader.getSheetsData();while (sheets.hasNext()) { InputStream sheetstream = sheets.next();//每一个sheet的数据流 InputSource sheetSource = new InputSource(sheetstream); try { parser.parse(sheetSource); } finally { sheetstream.close(); }}
让咱们应用Jvisualvm 看看事件驱动的形式是什么效率吧
通过简略的剖析以及运行两种模式进行比拟,能够看到用户模式下应用更简略的代码实现了Excel读取。
然而在读取大文件时CPU和内存都不现实;而事件模式尽管代码写起来比拟繁琐,然而在读取大文件时CPU和内存更加占优。