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

鸡汤

 希望是美好的,也许是人间至善,而美好的东西永不消逝。
                                         --《肖申克的救赎》

 当我们在实现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\下发现类似以下格式的文件:


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

    /**
     * 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);
    }

压缩后的临时文件如下:



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

未完待续

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

参考文档:https://poi.apache.org/components/spreadsheet/how-to.html#xssf_sax_api

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,951评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,606评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,601评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,478评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,565评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,587评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,590评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,337评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,785评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,096评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,273评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,935评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,578评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,199评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,440评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,163评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,133评论 2 352