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

相關文章
相關標籤/搜索