欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

基于流的EXCEL文件导出,SXSSFWorkbook源码解析

程序员文章站 2022-07-13 15:55:24
...

当我们在实现excel导出时,在数据量过大的情况下,总是容易发生内存溢出的情况。我们可以使用POI提供的 SXSSFWorkbook 类来避免内存溢出。

注:基于POI4.10版本源码

以下是官方文档对SXSSF包的说明

SXSSF (package: org.apache.poi.xssf.streaming) is an API-compatible streaming extension of XSSF to be used when very large spreadsheets have to be produced, and heap space is limited. SXSSF achieves its low memory footprint by limiting access to the rows that are within a sliding window, while XSSF gives access to all rows in the document. Older rows that are no longer in the window become inaccessible, as they are written to the disk.

大致翻译如下

SXSSF是XSSF的一个与API兼容的流扩展,在需要生成非常大的电子表格时使用,堆空间有限。SXSSF通过限制对滑动窗口中的行的访问来实现其低内存占用,而XSSF允许访问文档中的所有行。当将旧行写入磁盘时,不再在窗口中的旧行变得不可访问。

使用示例

        // 内存中保持100条数据, 超出的部分刷新到磁盘上
        SXSSFWorkbook wb = new SXSSFWorkbook(100);
     
        Sheet sh = wb.createSheet();
        for(int rownum = 0; rownum < 1000; rownum++){
            Row row = sh.createRow(rownum);
            for(int cellnum = 0; cellnum < 10; cellnum++){
                Cell cell = row.createCell(cellnum);
                String address = new CellReference(cell).formatAsString();
                cell.setCellValue(address);
            }

        }

        // rownum < 900 的数据被刷新到磁盘,不能被随机访问
        for(int rownum = 0; rownum < 900; rownum++){
            Assert.assertNull(sh.getRow(rownum));
        }

        // 最后的100条数据仍然在内存中,可以随机访问
        for(int rownum = 900; rownum < 1000; rownum++){
            Assert.assertNotNull(sh.getRow(rownum));
        }

        FileOutputStream out = new FileOutputStream("d:\\sxssf.xlsx");
        wb.write(out);
        out.close();

        // 从磁盘上释放临时文件
        wb.dispose();

临时文件分析

wb.write(out)此行断点,debug运行到此处时,可以在windows路径C:\Users\ADMINI~1\AppData\Local\Temp\下发现类似以下格式的文件:
基于流的EXCEL文件导出,SXSSFWorkbook源码解析

此文件就是被刷新到磁盘上的数据临时文件。此文件是怎么生成的呢?接下来我们就进入到源码分析的阶段。进入方法:

    /**
     * Sreate an Sheet for this Workbook, adds it to the sheets and returns
     * the high level representation.  Use this to create new sheets.
     *
     * @return Sheet representing the new sheet.
     */
    @Override
    public SXSSFSheet createSheet()
    {
        return createAndRegisterSXSSFSheet(_wb.createSheet());
    }

    SXSSFSheet createAndRegisterSXSSFSheet(XSSFSheet xSheet)
    {
        final SXSSFSheet sxSheet;
        try
        {
            sxSheet=new SXSSFSheet(this,xSheet);
        }
        catch (IOException ioe)
        {
            throw new RuntimeException(ioe);
        }
        registerSheetMapping(sxSheet,xSheet);
        return sxSheet;
    }

再进入new SXSSFSheet(this,xSheet)方法:

    public SXSSFSheet(SXSSFWorkbook workbook, XSSFSheet xSheet) throws IOException {
        _workbook = workbook;
        _sh = xSheet;
        _writer = workbook.createSheetDataWriter();
        setRandomAccessWindowSize(_workbook.getRandomAccessWindowSize());
        _autoSizeColumnTracker = new AutoSizeColumnTracker(this);
    }

接着进入workbook.createSheetDataWriter()方法,最后我们会发现以下代码:

    public SheetDataWriter() throws IOException {
        _fd = createTempFile();
        _out = createWriter(_fd);
    }

由此我们知道,在创建sheet页时,就创建了临时文件目录(即每一个sheet页都会对应创建一个临时文件)。那么临时文件是建立在什么目录下,是否可以手动修改呢?我们继续跟进_fd = createTempFile()方法:

    public File createTempFile() throws IOException {
        return TempFile.createTempFile("poi-sxssf-sheet", ".xml");
    }

上面代码我们可以知道,POI会生成一个前缀为’poi-sxssf-sheet’,后缀为’xml’的临时文件来存放表格数据的DOM结构。
POI提供了TempFileCreationStrategy接口的默认实现DefaultTempFileCreationStrategy来决定临时文件生成的目录:

    private void createPOIFilesDirectory() throws IOException {
        // Identify and create our temp dir, if needed
        // The directory is not deleted, even if it was created by this TempFileCreationStrategy
        if (dir == null) {
            String tmpDir = System.getProperty(JAVA_IO_TMPDIR);
            if (tmpDir == null) {
                throw new IOException("Systems temporary directory not defined - set the -D"+JAVA_IO_TMPDIR+" jvm property!");
            }
            dir = new File(tmpDir, POIFILES);
        }
        
        createTempDirectory(dir);
    }

JAVA_IO_TMPDIR实际上是JVM的系统变量java.io.tmpdir,由此我们可以知道SXSSF默认获取了JVM的临时文件目录来作为自己存放临时文件的目录。所以我们可以通过以下三种方式(推荐采用第三种)改变SXSSF临时文件的目录:

  • 设置JVM系统变量 -Djava.io.tmpdir=xxx 此方法会改变JVM所有的临时文件目录。
  • 实现TempFileCreationStrategy的接口
  • DefaultTempFileCreationStrategy的构造方法提供了 dir参数的构造:
    public DefaultTempFileCreationStrategy(File dir) { this.dir = dir; }
    重新构造DefaultTempFileCreationStrategy实例传值给org.apache.poi.util.TempFile类:
SXSSFWorkbook wb = new SXSSFWorkbook(100);
//更变临时文件目录
TempFile.setTempFileCreationStrategy(new DefaultTempFileCreationStrategy(new File("H:\\Temp")));
Sheet sh = wb.createSheet();
...
...
临时文件的压缩

SXSSF在临时文件中刷新表数据(每页一个临时文件),这些临时文件的大小可以增长到非常大的值。例如,对于20 MB的CSV数据,临时XML的大小超过了1000MB。如果临时文件的大小有问题,可以告诉SXSSF使用gzip压缩:

SXSSFWorkbook wb = new SXSSFWorkbook(100);
TempFile.setTempFileCreationStrategy(new DefaultTempFileCreationStrategy(new File("H:\\Temp")));
//压缩临时文件
wb.setCompressTempFiles(true);

SXSSFWorkbook类中有以下判断,我们可以看出,当设置了以上参数,SXSSF会用GZIP的方式压缩临时文件:

    protected SheetDataWriter createSheetDataWriter() throws IOException {
        if(_compressTmpFiles) {
            return new GZIPSheetDataWriter(_sharedStringSource);
        }
        
        return new SheetDataWriter(_sharedStringSource);
    }

压缩后的临时文件如下:
基于流的EXCEL文件导出,SXSSFWorkbook源码解析

可以看到,原本621MB的临时文件压缩后只有42MB。但是采用压缩显而易见的会影响到EXCEL导出的性能,期间的权衡应该以真实的业务场景来考虑。

实际上SXSSF所有对DOM文档的操作都直接映射在了XSSF上,只是在外层提供了刷新磁盘的功能。具体是如何实现的,且听下回分解。