基于Apache POI导出(百万级)大数据量Excel的实现

最近在做一个数据导出的功能,因为数据量比较大,所以需要考虑OOM的问题,再一个就是因为导出是一个常用功能,想将其做成组件化。所以查询了一些资料,写了一些小demo。最后完成了这个需求。

一、查资料

目标

支持单个 excel 的 sheet 导出100w 的数据

Apache POI操作Excel对象
1.HSSF:操作Excel 2007之前版本(.xls)格式,生成的EXCEL不经过压缩直接导出
2.XSSF:操作Excel 2007及之后版本(.xlsx)格式,内存占用高于HSSF
3.SXSSF:从POI3.8 beta3开始支持,基于XSSF,低内存占用,专门处理大数据量(建议)。
 注意: 值得注意的是SXSSFWorkbook只能写(导出)不能读(导入)
 说明:
(1) .xls格式的excel(最大行数65536行,最大列数256列)
(2) .xlsx格式的excel(最大行数1048576行,最大列数16384列)

结论:通过 POI的SXSSFWorkbook,使用操作系统的临时文件来作为缓存,可以生成超大的excel 文件,记得使用压缩。关键代码如下:

SXSSFWorkbook wb = null;
try {
    wb = new SXSSFWorkbook();
    wb.setCompressTempFiles(true); //压缩临时文件,很重要,否则磁盘很快就会被写满
    ...
} finally {
    if (wb != null) {
        wb.dispose();// 删除临时文件,很重要,否则磁盘可能会被写满
    }
}

官网地址:https://poi.apache.org/components/spreadsheet/how-to.html#sxssf

在 POI 的文档里发现了SXSSFWorkbook,其支持使用临时文件,可以用来生成超大 Excel 文件。

Since 3.8-beta3, POI provides a low-memory footprint SXSSF API built on top of XSSF.

SXSSF 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.

In auto-flush mode the size of the access window can be specified, to hold a certain
number of rows in memory. When that value is reached, the creation of an additional
row causes the row with the lowest index to to be removed from the access window and
written to disk. Or, the window size can be set to grow dynamically; it can be trimmed
periodically by an explicit call to flushRows(int keepRows) as needed.

Due to the streaming nature of the implementation, there are the following
limitations when compared to XSSF:
 * Only a limited number of rows are accessible at a point in time.
 * Sheet.clone() is not supported.
 * Formula evaluation is not supported

大概意思如下:

从3.8-beta3开始,POI提供了基于XSSF的低内存占用的SXSSF API。

SXSSF是XSSF的API兼容流扩展,可用于必须生成非常大的电子表格且堆空间有限的情况。SXSSF通过限制对滑动窗口内的行的访问来实现其低内存占用,而XSSF允许对文档中的所有行进行访问。不再在窗口中的较旧的行将被写入磁盘,因此无法访问。

在自动刷新模式下,可以指定访问窗口的大小,以在内存中保留一定数量的行。当达到该值时,附加行的创建将使索引最低的行从访问窗口中删除并写入磁盘。或者,可以将窗口大小设置为动态增长。可以根据需要通过显式调用flushRows(int keepRows)定期对其进行修剪。

由于实现的流性质,与XSSF相比存在以下限制:

  • 在某个时间点只能访问有限数量的行。
  • 不支持Sheet.clone()。
  • 不支持公式评估

SXSSFWorkbook在使用上有一些注意项

SXSSF flushes sheet data in temporary files (a temp file per sheet) and the size
of these temporary files can grow to a very large value. For example, for a 20 MB
csv data the size of the temp xml becomes more than a gigabyte. If the size of the
 temp files is an issue, you can tell SXSSF to use gzip compression:

  SXSSFWorkbook wb = new SXSSFWorkbook();
  wb.setCompressTempFiles(true); // temp files will be gzipped

大概意思如下:

SXSSF刷新临时文件中的工作表数据(每个sheet一个临时文件),这些临时文件的大小可能会增长到非常大的值。例如,对于20mb临时xml大小的csv数据变得超过十亿字节。如果临时文件的大小是一个问题,你可以告诉SXSSF使用gzip压缩:

SXSSFWorkbook wb =新SXSSFWorkbook();
wb.setCompressTempFiles(真正的);//临时文件将被gzip压缩

参考测试demo-01

try {
            long startTime = System.currentTimeMillis();
            final int NUM_OF_ROWS = rowsNum;
            final int NUM_OF_COLUMNS = 30;

            SXSSFWorkbook wb = null;
            try {
                wb = new SXSSFWorkbook();
                wb.setCompressTempFiles(true); //压缩临时文件,很重要,否则磁盘很快就会被写满
                Sheet sh = wb.createSheet();
                int rowNum = 0;
                for (int num = 0; num < NUM_OF_ROWS; num++) {
                    if (num % 100_0000 == 0) {
                        sh = wb.createSheet("sheet " + num);
                        rowNum = 0;
                    }
                    rowNum++;
                    Row row = sh.createRow(rowNum);
                    for (int cellnum = 0; cellnum < NUM_OF_COLUMNS; cellnum++) {
                        Cell cell = row.createCell(cellnum);
                        cell.setCellValue(Math.random());
                    }
                }

                FileOutputStream out = new FileOutputStream("ooxml-scatter-chart_SXSSFW_" + rowsNum + ".xlsx");
                wb.write(out);
                out.close();
            } catch (Exception ex) {
                ex.printStackTrace();
            } finally {
                if (wb != null) {
                    wb.dispose();// 删除临时文件,很重要,否则磁盘可能会被写满
                }
            }

            long endTime = System.currentTimeMillis();
            System.out.println("process " + rowsNum + " spent time:" + (endTime - startTime));
        } catch (Exception e) {
            e.printStackTrace();
            throw e;
        }

参考测试demo-02

1、ExcelUtils类:

package Utils;
 
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import org.apache.poi.common.usermodel.Hyperlink;
import org.apache.poi.hssf.usermodel.HSSFCellStyle;
import org.apache.poi.hssf.usermodel.HSSFFont;
import org.apache.poi.hssf.util.HSSFColor;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.CreationHelper;
import org.apache.poi.ss.usermodel.Font;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.xssf.streaming.SXSSFCell;
import org.apache.poi.xssf.streaming.SXSSFRow;
import org.apache.poi.xssf.streaming.SXSSFSheet;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import org.apache.poi.xssf.usermodel.XSSFHyperlink;
 
import java.io.IOException;
import java.io.OutputStream;
import java.math.BigDecimal;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;
 
/**
 * Created by Cheung on 2017/12/19.
 *
 * Apache POI操作Excel对象
 * HSSF:操作Excel 2007之前版本(.xls)格式,生成的EXCEL不经过压缩直接导出
 * XSSF:操作Excel 2007及之后版本(.xlsx)格式,内存占用高于HSSF
 * SXSSF:从POI3.8 beta3开始支持,基于XSSF,低内存占用,专门处理大数据量(建议)。
 *
 * 注意:
 *         值得注意的是SXSSFWorkbook只能写(导出)不能读(导入)
 *
 * 说明:
 *         .xls格式的excel(最大行数65536行,最大列数256列)
 *            .xlsx格式的excel(最大行数1048576行,最大列数16384列)
 */
public class ExcelUtil {
 
    public static final String DEFAULT_DATE_PATTERN = "yyyy年MM月dd日";// 默认日期格式
    public static final int DEFAULT_COLUMN_WIDTH = 17;// 默认列宽
 
 
    /**
     * 导出Excel(.xlsx)格式
     * @param titleList 表格头信息集合
     * @param dataArray 数据数组
     * @param os 文件输出流
     */
    public static void exportExcel(ArrayList titleList, JSONArray dataArray, OutputStream os) {
        String datePattern = DEFAULT_DATE_PATTERN;
        int minBytes = DEFAULT_COLUMN_WIDTH;
 
        /**
         * 声明一个工作薄
          */
        SXSSFWorkbook workbook = new SXSSFWorkbook(1000);// 大于1000行时会把之前的行写入硬盘
        workbook.setCompressTempFiles(true);
 
        // 表头1样式
        CellStyle title1Style = workbook.createCellStyle();
        title1Style.setAlignment(HSSFCellStyle.ALIGN_CENTER);// 水平居中
        title1Style.setVerticalAlignment(HSSFCellStyle.VERTICAL_CENTER);// 垂直居中
        Font titleFont = workbook.createFont();// 字体
        titleFont.setFontHeightInPoints((short) 20);
        titleFont.setBoldweight((short) 700);
        title1Style.setFont(titleFont);
 
        // 表头2样式
        CellStyle title2Style = workbook.createCellStyle();
        title2Style.setAlignment(HSSFCellStyle.ALIGN_CENTER);
        title2Style.setVerticalAlignment(HSSFCellStyle.VERTICAL_CENTER);
        title2Style.setBorderTop(HSSFCellStyle.BORDER_THIN);// 上边框
        title2Style.setBorderRight(HSSFCellStyle.BORDER_THIN);// 右
        title2Style.setBorderBottom(HSSFCellStyle.BORDER_THIN);// 下
        title2Style.setBorderLeft(HSSFCellStyle.BORDER_THIN);// 左
        Font title2Font = workbook.createFont();
        title2Font.setUnderline((byte) 1);
        title2Font.setColor(HSSFColor.BLUE.index);
        title2Style.setFont(title2Font);
 
        // head样式
        CellStyle headerStyle = workbook.createCellStyle();
        headerStyle.setAlignment(HSSFCellStyle.ALIGN_CENTER);
        headerStyle.setVerticalAlignment(HSSFCellStyle.VERTICAL_CENTER);
        headerStyle.setFillForegroundColor(HSSFColor.LIGHT_GREEN.index);// 设置颜色
        headerStyle.setFillPattern(HSSFCellStyle.SOLID_FOREGROUND);// 前景色纯色填充
        headerStyle.setBorderTop(HSSFCellStyle.BORDER_THIN);
        headerStyle.setBorderRight(HSSFCellStyle.BORDER_THIN);
        headerStyle.setBorderBottom(HSSFCellStyle.BORDER_THIN);
        headerStyle.setBorderLeft(HSSFCellStyle.BORDER_THIN);
        Font headerFont = workbook.createFont();
        headerFont.setFontHeightInPoints((short) 12);
        headerFont.setBoldweight(HSSFFont.BOLDWEIGHT_BOLD);
        headerStyle.setFont(headerFont);
 
        // 单元格样式
        CellStyle cellStyle = workbook.createCellStyle();
        cellStyle.setAlignment(HSSFCellStyle.ALIGN_CENTER);
        cellStyle.setVerticalAlignment(HSSFCellStyle.VERTICAL_CENTER);
        cellStyle.setBorderTop(HSSFCellStyle.BORDER_THIN);
        cellStyle.setBorderRight(HSSFCellStyle.BORDER_THIN);
        cellStyle.setBorderBottom(HSSFCellStyle.BORDER_THIN);
        cellStyle.setBorderLeft(HSSFCellStyle.BORDER_THIN);
        Font cellFont = workbook.createFont();
        cellFont.setBoldweight(HSSFFont.BOLDWEIGHT_NORMAL);
        cellStyle.setFont(cellFont);
 
 
        String title1 = (String) titleList.get(0).get("title1");
        String title2 = (String) titleList.get(0).get("title2");
        LinkedHashMap headMap = titleList.get(1);
 
        /**
         * 生成一个(带名称)表格
         */
        SXSSFSheet sheet = (SXSSFSheet) workbook.createSheet(title1);
        sheet.createFreezePane(0, 3, 0, 3);// (单独)冻结前三行
 
        /**
         * 生成head相关信息+设置每列宽度
         */
        int[] colWidthArr = new int[headMap.size()];// 列宽数组
        String[] headKeyArr = new String[headMap.size()];// headKey数组
        String[] headValArr = new String[headMap.size()];// headVal数组
        int i = 0;
        for (Map.Entry entry : headMap.entrySet()) {
            headKeyArr[i] = entry.getKey();
            headValArr[i] = entry.getValue();
 
            int bytes = headKeyArr[i].getBytes().length;
            colWidthArr[i] = bytes < minBytes ? minBytes : bytes;
            sheet.setColumnWidth(i, colWidthArr[i] * 256);// 设置列宽
            i++;
        }
 
 
        /**
         * 遍历数据集合,产生Excel行数据
          */
        int rowIndex = 0;
        for (Object obj : dataArray) {
            // 生成title+head信息
            if (rowIndex == 0) {
                SXSSFRow title1Row = (SXSSFRow) sheet.createRow(0);// title1行
                title1Row.createCell(0).setCellValue(title1);
                title1Row.getCell(0).setCellStyle(title1Style);
                sheet.addMergedRegion(new CellRangeAddress(0, 0, 0, headMap.size() - 1));// 合并单元格
 
                SXSSFRow title2Row = (SXSSFRow) sheet.createRow(1);// title2行
                title2Row.createCell(0).setCellValue(title2);
 
                CreationHelper createHelper = workbook.getCreationHelper();
                XSSFHyperlink  hyperLink = (XSSFHyperlink) createHelper.createHyperlink(Hyperlink.LINK_URL);
                hyperLink.setAddress(title2);
                title2Row.getCell(0).setHyperlink(hyperLink);// 添加超链接
 
                title2Row.getCell(0).setCellStyle(title2Style);
                sheet.addMergedRegion(new CellRangeAddress(1, 1, 0, headMap.size() - 1));// 合并单元格
 
                SXSSFRow headerRow = (SXSSFRow) sheet.createRow(2);// head行
                for (int j = 0; j < headValArr.length; j++) {
                    headerRow.createCell(j).setCellValue(headValArr[j]);
                    headerRow.getCell(j).setCellStyle(headerStyle);
                }
                rowIndex = 3;
            }
 
            JSONObject jo = (JSONObject) JSONObject.toJSON(obj);
            // 生成数据
            SXSSFRow dataRow = (SXSSFRow) sheet.createRow(rowIndex);// 创建行
            for (int k = 0; k < headKeyArr.length; k++) {
                SXSSFCell cell = (SXSSFCell) dataRow.createCell(k);// 创建单元格
                Object o = jo.get(headKeyArr[k]);
                String cellValue = "";
 
                if (o == null) {
                    cellValue = "";
                } else if (o instanceof Date) {
                    cellValue = new SimpleDateFormat(datePattern).format(o);
                } else if (o instanceof Float || o instanceof Double) {
                    cellValue = new BigDecimal(o.toString()).setScale(2, BigDecimal.ROUND_HALF_UP).toString();
                } else {
                    cellValue = o.toString();
                }
 
                if (cellValue.equals("true")) {
                    cellValue = "男";
                } else if (cellValue.equals("false")) {
                    cellValue = "女";
                }
 
                cell.setCellValue(cellValue);
                cell.setCellStyle(cellStyle);
            }
            rowIndex++;
        }
 
        try {
            workbook.write(os);
            os.flush();// 刷新此输出流并强制将所有缓冲的输出字节写出
            os.close();// 关闭流
            workbook.dispose();// 释放workbook所占用的所有windows资源
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
 
}

2、Student类:

package domain;
 
import java.util.Date;
 
/**
 * Created by Cheung on 2017/12/19.
 */
public class Student {
 
    private String name;
    private int age;
    private Date birthday;
    private float height;
    private double weight;
    private boolean sex;
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
    public int getAge() {
        return age;
    }
 
    public void setAge(int age) {
        this.age = age;
    }
 
    public Date getBirthday() {
        return birthday;
    }
 
    public void setBirthday(Date birthday) {
        this.birthday = birthday;
    }
 
    public float getHeight() {
        return height;
    }
 
    public void setHeight(float height) {
        this.height = height;
    }
 
    public double getWeight() {
        return weight;
    }
 
    public void setWeight(double weight) {
        this.weight = weight;
    }
 
    public boolean isSex() {
        return sex;
    }
 
    public void setSex(boolean sex) {
        this.sex = sex;
    }
}

3、ExcelExportTest类:

package controller;
 
import Utils.ExcelUtil;
import com.alibaba.fastjson.JSONArray;
import domain.Student;
 
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedHashMap;
 
/**
 * Created by Cheung on 2017/12/19.
 */
public class ExcelExportTest {
 
    public static void main(String[] args) throws IOException {
        // 模拟10W条数据
        int count = 100000;
         JSONArray studentArray = new JSONArray();
        for (int i = 0; i < count; i++) {
            Student s = new Student();
            s.setName("POI" + i);
            s.setAge(i);
            s.setBirthday(new Date());
            s.setHeight(i);
            s.setWeight(i);
            s.setSex(i % 2 == 0 ? false : true);
            studentArray.add(s);
        }
 
        ArrayList titleList = new ArrayList();
        LinkedHashMap titleMap = new LinkedHashMap();
        titleMap.put("title1","POI导出大数据量Excel Demo");
        titleMap.put("title2","https://github.com/550690513");
        LinkedHashMap headMap = new LinkedHashMap();
        headMap.put("name", "姓名");
        headMap.put("age", "年龄");
        headMap.put("birthday", "生日");
        headMap.put("height", "身高");
        headMap.put("weight", "体重");
        headMap.put("sex", "性别");
        titleList.add(titleMap);
        titleList.add(headMap);
 
 
 
        File file = new File("D://ExcelExportDemo/");
        if (!file.exists()) file.mkdir();// 创建该文件夹目录
        OutputStream os = null;
        Date date = new Date();
        try {
            // .xlsx格式
            os = new FileOutputStream(file.getAbsolutePath() + "/" + date.getTime() + ".xlsx");
            System.out.println("正在导出xlsx...");
            ExcelUtil.exportExcel(titleList, studentArray, os);
            System.out.println("导出完成...共" + count + "条数据,用时" + (new Date().getTime() - date.getTime()) + "ms");
            System.out.println("文件路径:" + file.getAbsolutePath() + "/" + date.getTime() + ".xlsx");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            os.close();
        }
 
    }
}


4、测试结果:10W条数据导出,用时3s多。


二、自己写demo测试

1.单个sheet100万行数据

package com;

import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.util.CellReference;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;

import java.io.FileOutputStream;
import java.util.Random;

/**
 * @Package: com
 * @ClassName: Test
 * @Description:
 * @author: 
 * @since: 2020/4/10 9:21
 * @version: 1.0
 * @Copyright: 2020 . All rights reserved.
 */
public class Test {

    public static void main(String[] args) throws Throwable {
        long start = System.currentTimeMillis();
        Random random = new Random();
        SXSSFWorkbook wb = new SXSSFWorkbook(100); // keep 100 rows in memory, exceeding rows will be flushed to disk
        wb.setCompressTempFiles(true); // temp files will be gzipped
        Sheet sh = wb.createSheet();
        for (int rownum = 0; rownum < 10000; rownum++) {
            Row row = sh.createRow(rownum);
            for (int cellnum = 0; cellnum < 100; cellnum++) {
                Cell cell = row.createCell(cellnum);
                String address = new CellReference(cell).formatAsString();
                cell.setCellValue(random.nextInt(10));
            }
        }

        System.out.println("开始导出====");
        FileOutputStream out = new FileOutputStream("D:\\temp\\zq.xlsx");
        wb.write(out);
        out.close();
        // dispose of temporary files backing this workbook on disk
        wb.dispose();
        System.out.println("完成导出====");
        long end = System.currentTimeMillis();
        System.out.println("导出文件耗时:" + (end - start) + "ms");
    }

}

2.100万行数据分成5个sheet

package com;

import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;

import java.io.FileOutputStream;
import java.util.Random;

/**
 * @Package: com
 * @ClassName: Test2
 * @Description:
 * @author: 
 * @since: 2020/4/10 10:31
 * @version: 1.0
 * @Copyright: 2020 . All rights reserved.
 */
public class Test2 {

    public static void main(String[] args) {

        try {
            long startTime = System.currentTimeMillis();
            final int NUM_OF_ROWS = 1000000;
            final int NUM_OF_COLUMNS = 100;
            Random random = new Random();
            SXSSFWorkbook wb = null;
            try {
                wb = new SXSSFWorkbook();
                wb.setCompressTempFiles(true); //压缩临时文件,很重要,否则磁盘很快就会被写满
                Sheet sh = null;
                //Sheet sh = wb.createSheet();
                int rowNum = 0;
                for (int num = 0; num < NUM_OF_ROWS; num++) {
                    if (num % 200000 == 0) {
                        sh = wb.createSheet("sheet " + (num / 200000 + 1));
                        rowNum = 0;
                    }

                    Row row = sh.createRow(rowNum);
                    for (int cellnum = 0; cellnum < NUM_OF_COLUMNS; cellnum++) {
                        Cell cell = row.createCell(cellnum);
                        cell.setCellValue(random.nextInt(10));
                    }
                    rowNum++;
                }

                FileOutputStream out = new FileOutputStream("D:\\temp\\zq2.xlsx");
                wb.write(out);
                out.close();
            } catch (Exception ex) {
                ex.printStackTrace();
            } finally {
                if (wb != null) {
                    wb.dispose();// 删除临时文件,很重要,否则磁盘可能会被写满
                }
            }

            long endTime = System.currentTimeMillis();
            System.out.println("process " + 1000000 + " spent time:" + (endTime - startTime));
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

}

三、最后形成的下载核心代码

 

你可能感兴趣的:(Apache,POI)