Java导出Excel

前言

众所周知,导Excel分为两步:css

  1. 抓取数据(查数据)
  2. 写数据到Excel文件

这两步都比较耗时间,通常咱们从数据库查数据,而后组装数据,最后写数据。html

查数据不是本节的重点,主要是SQL,索引这一块,此处不讨论。本节重点是写数据。数据库

问题

当数据量小(好比,几千几万条)的时候能够采用同步的方式,不用考虑别的。apache

而当数据量大的时候(好比,几十上百万)的时候问题就暴露出来了。多线程

首先,慢是确定的了。少则几十秒,多则几十分钟都是有可能的。app

这仍是小问题,最要命的由于一个导出把系统搞挂了。。。dom

笔者曾经见过,由于一个导出,系统直接挂了,还严重拖慢了同一台机器上的其它应用,最终宕机了。。。异步

究其缘由,大量数据堆积在内存中,可能会形成内存溢出。夸张一点,几百万条数据每条数据几十个字段都放到内存中,要等到所有写完这些内存才会释放。xss

方案

  • 针对单个工做表(sheet)的行数限制,能够分多个工做表
  • 针对单个文件太大不容易打开,能够分多个文件,最终打成压缩包
  • 针对内存溢出,能够分批导,每次导一批数据,分屡次导

建议

  1. 异步下载!异步!异步!异步!
  2. 若是对样式没什么要求,也不用公式的话,强烈推荐导出CSV格式
  3. 能够采用多线程的方式,先查总数,而后分一下看须要多少个线程,每一个线程读取一部数据并写入单独Excel文件;固然,也能够多线程读,单线程写
  4. 分批导,这一点跟上一步相似

思路

客户端发起下载请求之后,服务端异步执行下载任务并生成下载文件,客户端读取这个文件下载。ide

那么问题来了,客户端怎么知道服务端下载文件已经生成好了呢?

有一个方案是:WebSocket

客户端发起下载请求并收到服务端的响应之后和服务端创建一个WebSocket链接,这样服务端生成完文件之后就能够主动通知客户端了。

组件

关于导Excel的组件,笔者用过如下4种:

  • CSV
  • POI
  • JXLS
  • EasyPoi

其中,POS就不用说了,CSV真的很快,不熟悉CSV的请参考《Java导出CSV文件》,JXLS用模板的方式也很方便,能够预先定义好样式格式,easypoi是在poi基础上作了封装,使用注解就能轻松完成导出。

Apache POI

HSSF与XSSF基本用法

@Test
public void testHSSF() throws Exception {
    //  建立一个工做簿
    HSSFWorkbook wb = new HSSFWorkbook();
    //  建立一个工做表
    HSSFSheet sheet = wb.createSheet();

    //  建立字体
    HSSFFont font1 = wb.createFont();
    HSSFFont font2 = wb.createFont();
    font1.setFontHeightInPoints((short) 14);
    font1.setColor(HSSFColor.HSSFColorPredefined.RED.getIndex());
    font2.setFontHeightInPoints((short) 12);
    font2.setColor(HSSFColor.HSSFColorPredefined.BLUE.getIndex());
    //  建立单元格样式
    HSSFCellStyle css1 = wb.createCellStyle();
    HSSFCellStyle css2 = wb.createCellStyle();
    HSSFDataFormat df = wb.createDataFormat();
    //  设置单元格字体及格式
    css1.setFont(font1);
    css1.setDataFormat(df.getFormat("#,##0.0"));
    css2.setFont(font2);
    css2.setDataFormat(HSSFDataFormat.getBuiltinFormat("text"));

    //  建立行
    for (int i = 0; i < 20; i++) {
        HSSFRow row = sheet.createRow(i);
        for (int j = 0; j < 10; j = j + 2) {
            HSSFCell cell = row.createCell(j);
            cell.setCellValue("Spring");
            cell.setCellStyle(css1);

            HSSFCell cell2 = row.createCell(j+1);
            cell2.setCellValue(new HSSFRichTextString("Hello! " + j));
            cell2.setCellStyle(css2);
        }
    }

    //  写文件
    FileOutputStream fos = new FileOutputStream("G:/wb.xls");
    wb.write(fos);
    fos.close();
}

@Test
public void testSS() throws IOException {
    Workbook[] wbs = {new HSSFWorkbook(), new XSSFWorkbook()};
    for (int i = 0; i < wbs.length; i++) {
        Workbook wb = wbs[i];
        CreationHelper creationHelper = wb.getCreationHelper();
        Sheet sheet = wb.createSheet();
        for (int j = 0; j < 10; j++) {
            Row row = sheet.createRow(j);
            Cell cell = row.createCell(0);
            cell.setCellValue(creationHelper.createRichTextString("ABC"));
        }

        String filename = "G:/workbook.xls";
        if (wb instanceof XSSFWorkbook) {
            filename = filename + "x";
        }
        wb.write(new FileOutputStream(filename));
        wb.close();
    }
}

JXLS基本用法

@Test
public void abc() throws IOException {
    long t1 = System.currentTimeMillis();

    List<User> userList = new ArrayList<>();
    for (int i = 0; i < 100000; i++) {
        userList.add(new User("zhangsan", "10001"));
    }

    InputStream is = new FileInputStream("G:/object_collection_template.xlsx");
    OutputStream os = new FileOutputStream("G:/object_collection_out.xlsx");
    Context context = new Context();
    context.putVar("users", userList);
    JxlsHelper.getInstance().processTemplate(is, os, context);

    long t2 = System.currentTimeMillis();
    System.out.println(t2 - t1);
}

SXSSF

SXSSF扩展自XSSF,用于当很是大的工做表要导出且内存受限制的时候。SXSSF占用不多的内存是由于它限制只能访问滑动窗口中的数据,而XSSF能够访问文档中全部数据。那些不在滑动窗口中的数据是不能访问的,由于它们已经被写到磁盘上了。

你能够经过new SXSSFWorkbook(int windowSize)来指定窗口的大小,也能够经过SXSSFSheet#setRandomAccessWindowSize(int windowSize)来设置每一个工做表的窗口大小。

当经过createRow()建立一个新行的时候,总的行数可能会超过窗口大小,这个时候行号最低的那行会被刷新到磁盘并且不能经过getRow()访问。

默认的窗口大小是100。若是设置为-1,则表示不限,这就意味着没有记录会被自动刷新到磁盘,除非你手动调用flushRow()刷新。

注意,SXSSF会产生临时文件,你必须老是明确地清理它们,经过调用dispose方法。

/**
 * 写一个工做表,窗口大小是100
 * 当达到101行的时候,行号为0的行(rownum=0)被刷新到磁盘,并从内存中删除
 * 当行号达到102的时候,rownum=1的行被刷新到磁盘,并从内存中删除
 * 也就是说内存中最多保存100行,就是一个滑动窗口
 */
@Test
public void testWindow() throws IOException {
    //  在内存中保存100行,当行数超过100时将其刷新到磁盘
    System.out.println(Runtime.getRuntime().freeMemory());

    SXSSFWorkbook wb = new SXSSFWorkbook(100);
    SXSSFSheet sheet = wb.createSheet();
    for (int i = 0; i < 1000; i++) {
        SXSSFRow row = sheet.createRow(i);
        for (int j = 0; j < 10; j++) {
            SXSSFCell cell = row.createCell(j);
            cell.setCellValue(new CellReference(cell).formatAsString());
        }
    }

    //  行号小于900的行已经被刷新到磁盘,没法访问
    for (int rownum = 0; rownum < 900; rownum++) {
        Assert.assertNull(sheet.getRow(rownum));
    }

    //  最后100行仍然在内存中
    for (int rownum = 900; rownum < 1000; rownum++) {
        Assert.assertNotNull(sheet.getRow(rownum));
    }

    FileOutputStream fos = new FileOutputStream("G:/sxssf.xlsx");
    wb.write(fos);
    fos.close();

    //  处理工做表在磁盘上产生的临时文件
    wb.dispose();
}

/**
 * 关闭自动刷新,而且手动控制哪些数据被写到磁盘
 */
@Test
public void testAutoFlush() throws IOException {
    //  关闭自动刷新,而且在内存中累积全部的行
    SXSSFWorkbook wb = new SXSSFWorkbook(-1);
    SXSSFSheet sheet = wb.createSheet();
    for (int rownum = 0; rownum < 1000; rownum++) {
        Row row = sheet.createRow(rownum);
        for (int cellnum = 0; cellnum < 10; cellnum++) {
            Cell cell = row.createCell(cellnum);
            cell.setCellValue(new CellReference(cell).formatAsString());
        }

        //  手动控制刷新多少行到磁盘
        if (rownum % 100 == 0) {
            //  保留最后100行,其他的刷新到磁盘
            sheet.flushRows(100);
//                sheet.flushRows();  //  全部行,所有刷新到磁盘
        }
    }

    FileOutputStream fos = new FileOutputStream("G:/sxssf2.xlsx");
    wb.write(fos);
    fos.close();

    //  删除产生的临时文件
    wb.dispose();
}

/**
 * SXSSF刷新工做表数据到磁盘(每一个工做表一个临时文件),并且,临时文件可能会增加到很是大。
 * 例如,对于一个20M的csv数据它的临时xml数据有可能会变得超过1G
 * 若是你任务临时文件的的大小是一个问题的话,那么你能够告诉SXSSF用gzip来压缩它。
 * SXSSFWorkbook wb = new SXSSFWorkbook();
 * wb.setCompressTempFiles(true); // temp files will be gzipped
 */

Maven依赖

<dependencies>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-csv</artifactId>
        <version>1.5</version>
    </dependency>

    <dependency>
        <groupId>org.apache.poi</groupId>
        <artifactId>poi</artifactId>
        <version>3.17</version>
    </dependency>

    <dependency>
        <groupId>org.apache.poi</groupId>
        <artifactId>poi-ooxml</artifactId>
        <version>3.17</version>
    </dependency>

    <dependency>
        <groupId>org.jxls</groupId>
        <artifactId>jxls</artifactId>
        <version>2.4.5</version>
    </dependency>

    <dependency>
        <groupId>org.jxls</groupId>
        <artifactId>jxls-poi</artifactId>
        <version>1.0.15</version>
    </dependency>


    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
        <scope>test</scope>
    </dependency>
</dependencies>

参考

http://www.javashuo.com/article/p-kegxkbke-nq.html

https://poi.apache.org/spreadsheet/quick-guide.html

https://poi.apache.org/spreadsheet/how-to.html#sxssf

http://jxls.sourceforge.net/getting_started.html

http://www.javashuo.com/article/p-kmenmjon-nq.html

http://www.javashuo.com/article/p-emlozsjx-nd.html

http://www.javashuo.com/article/p-uzaindcw-bn.html

http://www.javashuo.com/article/p-tftmawvw-dk.html

相关文章
相关标签/搜索