超大Excel文件读取(支持50w+)(三)

超大Excel文件读取(支持50w+)

1 线上内存溢出问题演示

环境准备

  • 准备一个大的excel文件(xlsx大小10M及以上)
  • 将jvm的heap缩小到500m(JVM 参数 -Xmx500m)用于模拟OOM
  • 使用参数在OOM时dump内存快照 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=d://dump.hprof

依赖管理

		
		<dependency>
			<groupId>org.apache.poigroupId>
			<artifactId>poi-ooxmlartifactId>
			<version>4.1.0version>
		dependency>

		
		<dependency>
			<groupId>xercesgroupId>
			<artifactId>xercesImplartifactId>
			<version>2.11.0version>
		dependency>

运行代码

/**
 * 超大excel读取示例
 *
 * @author Leon
 * @version 2020/5/5 20:07
 */
public class ReadExcelDemo
{
	public static void main(String[] args) throws Exception
	{
		FileInputStream is = new FileInputStream("d:\\test.xlsx");
		XSSFWorkbook wb = new XSSFWorkbook(is);
		// TODO with wb
		System.out.println("ok");
	}
}

运行结果

java.lang.OutOfMemoryError: Java heap space
Dumping heap to d://dump.hprof ...
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at java.io.ByteArrayOutputStream.(ByteArrayOutputStream.java:77)
	at org.apache.poi.util.IOUtils.toByteArray(IOUtils.java:147)
	at org.apache.poi.util.IOUtils.toByteArray(IOUtils.java:121)
	at org.apache.poi.openxml4j.util.ZipArchiveFakeEntry.(ZipArchiveFakeEntry.java:47)
	at org.apache.poi.openxml4j.util.ZipInputStreamZipEntrySource.(ZipInputStreamZipEntrySource.java:53)
	at org.apache.poi.openxml4j.opc.ZipPackage.(ZipPackage.java:106)
	at org.apache.poi.openxml4j.opc.OPCPackage.open(OPCPackage.java:301)
	at org.apache.poi.ooxml.util.PackageHelper.open(PackageHelper.java:37)
	at org.apache.poi.xssf.usermodel.XSSFWorkbook.(XSSFWorkbook.java:303)
	at com.concurrent.excel.ReadExcelDemo.main(ReadExcelDemo.java:20)

dump.hprof文件分析

使用jdk自带的jvisualvm打开此文件。如下图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-arAxXn4Z-1588769902970)(./asserts/001.png)]

原因总结:处理xlsx时会将数据完全读入内存,导致内存溢出。根据笔者的经验,10m的excel在后台完全处理的情况至少需要2GB内存,这个
放大的倍数还是很吓人的,怪不得造成内存溢出。

2 小内存实现超大excel数据读取

上一小节,我们已经定位在OOM的原因,针对这种情况,POI官方也给出了解决方案。就是事件驱动模式(Event API)。

  • 官网的相关链接: POI官网链接及示例代码
  • 官网的相关链接: POI issue列表 请看第14个

示例代码

此代码可以直接拷贝使用或者稍加改造即可在项目中使用。


/**
 * 事件驱动:超大excel读取示例
 *
 * @author Leon
 * @version 2020/5/5 20:07
 */
public class EventReadExcelDemo
{
	public static void main(String[] args) throws Exception
	{
		new EventReadExcelDemo().processOneSheet("d:\\test.xlsx");
		System.out.println("ok");
	}

	public void processOneSheet(String filename) throws Exception {
		OPCPackage pkg = OPCPackage.open(filename);
		XSSFReader r = new XSSFReader(pkg);
		SharedStringsTable sst = r.getSharedStringsTable();

		XMLReader parser = fetchSheetParser(sst);

		// 获得第一个sheet
		InputStream sheet2 = r.getSheet("rId1");
		InputSource sheetSource = new InputSource(sheet2);
		parser.parse(sheetSource);
		sheet2.close();
	}

	public XMLReader fetchSheetParser(SharedStringsTable sst) throws SAXException {
		XMLReader parser =
			XMLReaderFactory.createXMLReader(
				"org.apache.xerces.parsers.SAXParser"
			);
		ContentHandler handler = new SheetHandler(sst);
		parser.setContentHandler(handler);
		return parser;
	}

	private static class SheetHandler extends DefaultHandler {
		private SharedStringsTable sst;
		private String lastContents;
		private boolean nextIsString;

		private SheetHandler(SharedStringsTable sst) {
			this.sst = sst;
		}

		//元素开始时的handler
		public void startElement(String uri, String localName, String name,
		                         Attributes attributes) throws SAXException {
			// c => 单元格
			if(name.equals("c")) {
				System.out.print(attributes.getValue("r") + " - ");
				// 获取单元格类型
				String cellType = attributes.getValue("t");
				if(cellType != null && cellType.equals("s")) {
					nextIsString = true;
				} else {
					nextIsString = false;
				}
			}
			lastContents = "";
		}

		//元素结束时的handler
		public void endElement(String uri, String localName, String name)
			throws SAXException {
			if(nextIsString) {
				int idx = Integer.parseInt(lastContents);
				lastContents = new XSSFRichTextString(sst.getEntryAt(idx)).toString();
				nextIsString = false;
			}

			// v => 单元格内容
			if(name.equals("v")) {
				System.out.println(lastContents);
			}
		}

		//读取元素间内容时的handler
		public void characters(char[] ch, int start, int length)
			throws SAXException {
			lastContents += new String(ch, start, length);
		}
	}
}

运行结果

....
E153797 - A153798 - xxxxx信息内容
B153798 - 2953
C153798 - 落叶灌木
D153798 - 木芙蓉
...

我们发现: 运行了到15万行了,也没有发生内存溢出。

3 小结

同样的使用这种方法可以流式读取打的xlsx文件,但是只限于读取内部的数据,而且无法进行修改操作

之后会介绍写大文件的方法。

你可能感兴趣的:(Java基础,Excel,读取,poi超大数据读取,超大excel导入,excel导入,最新版)