記一次 Java 匯出大批量 Excel 優化

光頭才能強 發表於 2021-11-07
Java Excel

常用的excel匯出方案,詳情見Spring Boot 入門(十二):報表匯出,對比poi、jxl和esayExcel的效率,其中jxl、esayEscel 底層都是基於 poi,它們僅僅是對 poi 進行了封裝,使匯出 excel 的程式碼更加簡潔(封裝 poi,匯出 excel 的工具類有很多,jxl 和 esayExcel 使用的比較多)。所以,如果遇到 excel 匯出效率低下,直接基於 poi 匯出 Excel,效率肯定是最高的,只是程式碼比較繁瑣與雜亂。

我主要是基於 esayExcel (目前使用比較多),對其 api 進行衍生與應用,解決匯出大量 Excel 導致的 OOM,或者資料超界異常:

java.lang.IllegalArgumentException: Invalid row number (1048576) outside allowable range (0..1048575)

應用是基於100W條資料進行的測試,資料的獲取程式碼如下

 1 package bean;
 2 
 3 import com.alibaba.excel.annotation.ExcelProperty;
 4 import com.alibaba.excel.annotation.format.DateTimeFormat;
 5 import com.alibaba.excel.annotation.format.NumberFormat;
 6 import lombok.Data;
 7 import lombok.experimental.Accessors;
 8 
 9 import java.util.Date;
10 
11 /**
12  * @author dz
13  * @date 2021-11-06 上午 9:14
14  */
15 @Accessors(chain = true)
16 @Data
17 public class ExcelBean {
18 
19     @ExcelProperty("主鍵id")
20     private String id;
21 
22     @ExcelProperty("姓名")
23     private String name;
24 
25     @ExcelProperty("地址")
26     private String address;
27 
28     @ExcelProperty("年齡")
29     private Integer age;
30 
31     @ExcelProperty("數量")
32     private Integer number;
33 
34     @NumberFormat("#.##")
35     @ExcelProperty("身高")
36     private Double high;
37 
38     @ExcelProperty("距離")
39     private Double distance;
40 
41     @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
42     @ExcelProperty("開始時間")
43     private Date startTime;
44 
45     @ExcelProperty("結束時間")
46     private Date endTime;
47 }

 

 1 /**
 2      * 獲取excel 匯出的資料
 3      *
 4      * @return list 集合
 5      */
 6     private List<ExcelBean> getDate() {
 7         log.info("開始生成資料");
 8         Date date = new Date();
 9         long startTime = System.currentTimeMillis();
10         List<ExcelBean> list = Lists.newArrayList();
11         for (int i = 0; i < 1000000; i++) {
12             ExcelBean bean = new ExcelBean();
13             bean.setId(UUID.randomUUID().toString()).
14                     setName("隔壁老樊" + i).
15                     setAddress("北京市朝陽區酒仙橋" + i + "路").
16                     setAge(i).
17                     setNumber(i + 10000).
18                     setHigh(1.234 * i).
19                     setDistance(1.234 * i).
20                     setStartTime(date).
21                     setEndTime(date);
22             list.add(bean);
23         }
24         log.info("資料生成結束,資料量={},耗時={}ms", list.size(), System.currentTimeMillis() - startTime);
25         return list;
26     }

 

pom 依賴

1         <dependency>
2             <groupId>org.projectlombok</groupId>
3             <artifactId>lombok</artifactId>
4         </dependency>
5         <dependency>
6             <groupId>com.alibaba</groupId>
7             <artifactId>easyexcel</artifactId>
8             <version>2.2.10</version>
9         </dependency>

依賴 esayexcel 時,如果專案已經依賴了 poi,有可能會產生jar 包依賴衝突(easyexcel底層也是基於 poi),解決方案如下:

方案一:檢視 easyexcel 中依賴的 poi 的版本,然後將專案其餘地方的版本修改成該版本,使專案依賴的 poi 版本和 easyexcel 依賴的版本一致

方案二:在 esayexcel 中將 poi 的依賴排除掉,如下

 <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>easyexcel</artifactId>
            <version>2.2.10</version>
            <exclusions>
                <exclusion>
                    <groupId>org.apache.poi</groupId>
                    <artifactId>poi</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.apache.poi</groupId>
                    <artifactId>poi-ooxml</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.apache.poi</groupId>
                    <artifactId>poi-ooxml-schemas</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

 

常量的定義

1     public static final String FILE_NAME = "D:\\test_" + System.currentTimeMillis() + ".xlsx";
2     // 每個 sheet 寫入的資料
3     public static final int NUM_PER_SHEET = 300000;
4     // 每次向 sheet 中寫入的資料(分頁寫入)
5     public static final int NUM_BY_TIMES = 50000;

 

1.EasyExcel 匯出 excel 應用

使用 esayExcel 自帶的 api 匯出 excel 的應用,程式碼如下

 1     /**
 2      * 方法一:將資料寫入到excel
 3      * 直接呼叫api,適合小資料量
 4      * 100W條資料33s
 5      */
 6     @Test
 7     public void writeExcelByApi() {
 8         String fileName = FILE_NAME;
 9         log.info("匯出excel名稱={}", fileName);
10         long startTime = System.currentTimeMillis();
11         // 直接呼叫api
12         List<ExcelBean> date = getDate();
13         EasyExcel.write(fileName, ExcelBean.class).sheet().doWrite(date);
14         log.info("匯出excel結束,資料量={},耗時={}ms", date.size(), System.currentTimeMillis() - startTime);
15     }

 

當 list 物件資料量太多,就會產生異常:原因是單個 excel 一個 sheet 的最大資料量為 1048575

 1 java.lang.IllegalArgumentException: Invalid row number (1048576) outside allowable range (0..1048575)
 2 
 3     at org.apache.poi.xssf.streaming.SXSSFSheet.createRow(SXSSFSheet.java:123)
 4     at org.apache.poi.xssf.streaming.SXSSFSheet.createRow(SXSSFSheet.java:65)
 5     at com.alibaba.excel.util.WorkBookUtil.createRow(WorkBookUtil.java:70)
 6     at com.alibaba.excel.write.executor.ExcelWriteAddExecutor.addOneRowOfDataToExcel(ExcelWriteAddExecutor.java:67)
 7     at com.alibaba.excel.write.executor.ExcelWriteAddExecutor.add(ExcelWriteAddExecutor.java:56)
 8     at com.alibaba.excel.write.ExcelBuilderImpl.addContent(ExcelBuilderImpl.java:58)
 9     at com.alibaba.excel.ExcelWriter.write(ExcelWriter.java:161)
10     at com.alibaba.excel.ExcelWriter.write(ExcelWriter.java:146)
11     at com.alibaba.excel.write.builder.ExcelWriterSheetBuilder.doWrite(ExcelWriterSheetBuilder.java:61)
12     at mytest.TestExcel.writeExcelByApi(TestExcel.java:40)
13     at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
14     at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
15     at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
16     at java.lang.reflect.Method.invoke(Method.java:498)
17     at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
18     at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
19     at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
20     at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
21     at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
22     at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
23     at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
24     at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
25     at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
26     at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
27     at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
28     at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
29     at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
30     at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
31     at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
32     at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
33     at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
34     at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:69)
35     at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
36     at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:220)
37     at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:53)

 

2.EasyExcel 匯出 excel 應用優化一:execl 資料量超過1048575

將資料寫入到不同的 sheet,保證每個 sheet 的資料量小於 1048575 行,解決此問題,程式碼如下

 1 /**
 2      * 方法二:匯出多個sheet
 3      * easyExcel 底層是 POI 實現的,POI 單個sheet 最多隻能匯出 1048576 行,超過該行數,會產生如下異常
 4      * java.lang.IllegalArgumentException: Invalid row number (1048576) outside allowable range (0..1048575)
 5      * <p>
 6      * 11:57:55.541 [main] INFO mytest.TestExcel - 寫入sheet=sheet0,資料量300000-0=300000,耗時=6055ms
 7      * 11:57:59.701 [main] INFO mytest.TestExcel - 寫入sheet=sheet1,資料量600000-300000=300000,耗時=4159ms
 8      * 11:58:03.827 [main] INFO mytest.TestExcel - 寫入sheet=sheet2,資料量900000-600000=300000,耗時=4126ms
 9      * 11:58:05.193 [main] INFO mytest.TestExcel - 寫入sheet=sheet3,資料量1000000-900000=100000,耗時=1366ms
10      * 11:58:17.418 [main] INFO mytest.TestExcel - 匯出excel結束,總資料量=1000000,耗時=31297ms
11      */
12     @Test
13     public void writeExcelByMulSheet() {
14         String fileName = FILE_NAME;
15         log.info("匯出excel名稱={}", fileName);
16         long startTime = System.currentTimeMillis();
17         // 獲取資料
18         List<ExcelBean> date = getDate();
19         // 獲取 sheet 的個數
20         int sheetNum = date.size() % NUM_PER_SHEET == 0 ? date.size() / NUM_PER_SHEET : date.size() / NUM_PER_SHEET + 1;
21         // 指定寫入的檔案
22         ExcelWriter excelWriter = EasyExcel.write(fileName, ExcelBean.class).build();
23         for (int i = 0; i < sheetNum; i++) {
24             long l = System.currentTimeMillis();
25             // 設定 sheet 的名字(sheet不能相同)
26             String sheetName = "sheet" + i;
27             WriteSheet writeSheet = EasyExcel.writerSheet(i, sheetName).build();
28             int startNum = i * NUM_PER_SHEET;
29             int endNum = i == sheetNum - 1 ? date.size() : (i + 1) * NUM_PER_SHEET;
30             excelWriter.write(date.subList(startNum, endNum), writeSheet);
31             log.info("寫入sheet={},資料量{}-{}={},耗時={}ms", sheetName, endNum, startNum, endNum - startNum, System.currentTimeMillis() - l);
32         }
33         // 最好放在 finally中
34         excelWriter.finish();
35         log.info("匯出excel結束,總資料量={},耗時={}ms", date.size(), System.currentTimeMillis() - startTime);
36     }

 

3.EasyExcel 匯出 excel 應用優化二:資料來源 list 太大,直接讀取全部的 list 資料導致 OOM

將 list 資料進行分頁讀取,並進行分頁寫入到 excel。這樣還有個好處,每次每頁讀取部分資料,然後寫入到 excel 中(相當於該批資料已經從記憶體刷到了磁碟),也增加了寫入的效率;poi 中的匯出excel,為此專門提供了一個重新整理磁碟的 api,具體程式碼如下

 1 /**
 2      * 方法三:同一個 Sheet,分批多次寫入
 3      * 當單次讀取的 list 資料量過大,會產生 OOM 異常,所以需要分頁讀取並寫入到 excel
 4      * 11:55:01.590 [main] INFO mytest.TestExcel - 寫入數量50000-0=50000,耗時=2227ms
 5      * 11:55:02.429 [main] INFO mytest.TestExcel - 寫入數量100000-50000=50000,耗時=838ms
 6      * 11:55:03.188 [main] INFO mytest.TestExcel - 寫入數量150000-100000=50000,耗時=759ms
 7      * 11:55:03.951 [main] INFO mytest.TestExcel - 寫入數量200000-150000=50000,耗時=762ms
 8      * 11:55:04.708 [main] INFO mytest.TestExcel - 寫入數量250000-200000=50000,耗時=757ms
 9      * 11:55:05.471 [main] INFO mytest.TestExcel - 寫入數量300000-250000=50000,耗時=762ms
10      * 11:55:06.172 [main] INFO mytest.TestExcel - 寫入數量350000-300000=50000,耗時=701ms
11      * 11:55:06.921 [main] INFO mytest.TestExcel - 寫入數量400000-350000=50000,耗時=749ms
12      * 11:55:07.688 [main] INFO mytest.TestExcel - 寫入數量450000-400000=50000,耗時=767ms
13      * 11:55:08.437 [main] INFO mytest.TestExcel - 寫入數量500000-450000=50000,耗時=749ms
14      * 11:55:09.141 [main] INFO mytest.TestExcel - 寫入數量550000-500000=50000,耗時=704ms
15      * 11:55:09.899 [main] INFO mytest.TestExcel - 寫入數量600000-550000=50000,耗時=758ms
16      * 11:55:10.597 [main] INFO mytest.TestExcel - 寫入數量650000-600000=50000,耗時=698ms
17      * 11:55:11.353 [main] INFO mytest.TestExcel - 寫入數量700000-650000=50000,耗時=756ms
18      * 11:55:12.055 [main] INFO mytest.TestExcel - 寫入數量750000-700000=50000,耗時=701ms
19      * 11:55:12.820 [main] INFO mytest.TestExcel - 寫入數量800000-750000=50000,耗時=765ms
20      * 11:55:13.576 [main] INFO mytest.TestExcel - 寫入數量850000-800000=50000,耗時=756ms
21      * 11:55:14.287 [main] INFO mytest.TestExcel - 寫入數量900000-850000=50000,耗時=711ms
22      * 11:55:15.055 [main] INFO mytest.TestExcel - 寫入數量950000-900000=50000,耗時=768ms
23      * 11:55:15.773 [main] INFO mytest.TestExcel - 寫入數量1000000-950000=50000,耗時=718ms
24      * 11:55:28.016 [main] INFO mytest.TestExcel - 匯出excel結束,總資料量=1000000,耗時=31738ms
25      *
26      * Process finished with exit code 0
27      */
28     @Test
29     public void writeExcelByMulWrite() {
30         String fileName = FILE_NAME;
31         log.info("匯出excel名稱={}", fileName);
32         long startTime = System.currentTimeMillis();
33         // 獲取資料
34         List<ExcelBean> date = getDate();
35         ExcelWriter excelWrite = EasyExcel.write(fileName, ExcelBean.class).build();
36         WriteSheet writeSheet = EasyExcel.writerSheet("testSheet").build();
37         // 計算需要寫入的次數
38         int times = date.size() % NUM_BY_TIMES == 0 ? date.size() / NUM_BY_TIMES : date.size() / NUM_BY_TIMES + 1;
39         for (int i = 0; i < times; i++) {
40             long l = System.currentTimeMillis();
41             int startNum = i * NUM_BY_TIMES;
42             int endNum = i == times - 1 ? date.size() : (i + 1) * NUM_BY_TIMES;
43             excelWrite.write(date.subList(startNum, endNum), writeSheet);
44             log.info("寫入數量{}-{}={},耗時={}ms", endNum, startNum, endNum - startNum, System.currentTimeMillis() - l);
45         }
46         // 需要放入 finally 中
47         if (excelWrite != null) {
48             excelWrite.finish();
49         }
50         log.info("匯出excel結束,總資料量={},耗時={}ms", date.size(), System.currentTimeMillis() - startTime);
51     }

 

4.EasyExcel 匯出 excel 應用優化三:結合前面兩種方案

將 list 資料進行分頁讀取,並且每個 sheet 分多次寫入,且寫入到多個 sheet 中

 1  /**
 2      * 方案四:寫入多個 sheet,並且每個 sheet 寫入多次資料(結合方案二、三)
 3      * 資料量大,導致一個 sheet 儲存不下;同時單次讀入的資料量太大。可以採用這個方法
4 * 12:02:18.751 [main] INFO mytest.TestExcel - 寫入sheet=sheet0,資料量=50000-0=50000,耗時=1558 5 * 12:02:19.542 [main] INFO mytest.TestExcel - 寫入sheet=sheet0,資料量=100000-50000=50000,耗時=791 6 * 12:02:20.282 [main] INFO mytest.TestExcel - 寫入sheet=sheet0,資料量=150000-100000=50000,耗時=740 7 * 12:02:21.037 [main] INFO mytest.TestExcel - 寫入sheet=sheet0,資料量=200000-150000=50000,耗時=755 8 * 12:02:21.781 [main] INFO mytest.TestExcel - 寫入sheet=sheet0,資料量=250000-200000=50000,耗時=744 9 * 12:02:22.524 [main] INFO mytest.TestExcel - 寫入sheet=sheet0,資料量=300000-250000=50000,耗時=742 10 * 12:02:23.201 [main] INFO mytest.TestExcel - 寫入sheet=sheet1,資料量=350000-300000=50000,耗時=677 11 * 12:02:23.852 [main] INFO mytest.TestExcel - 寫入sheet=sheet1,資料量=400000-350000=50000,耗時=651 12 * 12:02:24.451 [main] INFO mytest.TestExcel - 寫入sheet=sheet1,資料量=450000-400000=50000,耗時=599 13 * 12:02:25.100 [main] INFO mytest.TestExcel - 寫入sheet=sheet1,資料量=500000-450000=50000,耗時=649 14 * 12:02:25.753 [main] INFO mytest.TestExcel - 寫入sheet=sheet1,資料量=550000-500000=50000,耗時=653 15 * 12:02:26.350 [main] INFO mytest.TestExcel - 寫入sheet=sheet1,資料量=600000-550000=50000,耗時=597 16 * 12:02:26.995 [main] INFO mytest.TestExcel - 寫入sheet=sheet2,資料量=650000-600000=50000,耗時=645 17 * 12:02:27.588 [main] INFO mytest.TestExcel - 寫入sheet=sheet2,資料量=700000-650000=50000,耗時=593 18 * 12:02:28.244 [main] INFO mytest.TestExcel - 寫入sheet=sheet2,資料量=750000-700000=50000,耗時=656 19 * 12:02:28.893 [main] INFO mytest.TestExcel - 寫入sheet=sheet2,資料量=800000-750000=50000,耗時=648 20 * 12:02:29.506 [main] INFO mytest.TestExcel - 寫入sheet=sheet2,資料量=850000-800000=50000,耗時=613 21 * 12:02:30.163 [main] INFO mytest.TestExcel - 寫入sheet=sheet2,資料量=900000-850000=50000,耗時=657 22 * 12:02:30.760 [main] INFO mytest.TestExcel - 寫入sheet=sheet3,資料量=950000-900000=50000,耗時=597 23 * 12:02:31.419 [main] INFO mytest.TestExcel - 寫入sheet=sheet3,資料量=1000000-950000=50000,耗時=659 24 * 12:02:43.235 [main] INFO mytest.TestExcel - 匯出excel結束,總資料量=1000000,耗時=28818ms 25 * 26 * Process finished with exit code 0 27 */ 28 @Test 29 public void writeExcelByMulSheetAndWriteChange() { 30 String fileName = FILE_NAME; 31 log.info("匯出excel名稱={}", fileName); 32 long startTime = System.currentTimeMillis(); 33 // 獲取資料 34 List<ExcelBean> date = getDate(); 35 // 獲取 sheet 的個數 36 int sheetNum = date.size() % NUM_PER_SHEET == 0 ? date.size() / NUM_PER_SHEET : date.size() / NUM_PER_SHEET + 1; 37 // 獲取每個sheet 寫入的次數 38 int writeNumPerSheet = NUM_PER_SHEET % NUM_BY_TIMES == 0 ? NUM_PER_SHEET / NUM_BY_TIMES : NUM_PER_SHEET / NUM_BY_TIMES + 1; 39 // 最後一個 sheet 寫入的數量 40 int writeNumLastSheet = date.size() - (sheetNum - 1) * NUM_PER_SHEET; 41 // 最後一個 sheet 寫入的次數 42 int writeNumPerLastSheet = writeNumLastSheet % NUM_BY_TIMES == 0 ? writeNumLastSheet / NUM_BY_TIMES : writeNumLastSheet / NUM_BY_TIMES + 1; 43 // 指定寫入的檔案 44 ExcelWriter excelWriter = EasyExcel.write(fileName, ExcelBean.class).build(); 45 for (int i = 0; i < sheetNum; i++) { 46 String sheetName = "sheet" + i; 47 WriteSheet writeSheet = EasyExcel.writerSheet(i, sheetName).build(); 48 int writeNum = i == sheetNum - 1 ? writeNumPerLastSheet : writeNumPerSheet; // 每個sheet 寫入的次數 49 int endEndNum = i == sheetNum - 1 ? date.size() : (i + 1) * NUM_PER_SHEET; // 每個sheet 最後一次寫入的最後行數 50 for (int j = 0; j < writeNum; j++) { 51 long l = System.currentTimeMillis(); 52 int startNum = i * NUM_PER_SHEET + j * NUM_BY_TIMES; 53 int endNum = j == writeNum - 1 ? endEndNum : i * NUM_PER_SHEET + (j + 1) * NUM_BY_TIMES; 54 excelWriter.write(date.subList(startNum, endNum), writeSheet); 55 log.info("寫入sheet={},資料量={}-{}={},耗時={}", sheetName, endNum, startNum, endNum - startNum, System.currentTimeMillis() - l); 56 } 57 } 58 // 需要放入 finally 中 59 if (excelWriter != null) { 60 excelWriter.finish(); 61 } 62 log.info("匯出excel結束,總資料量={},耗時={}ms", date.size(), System.currentTimeMillis() - startTime); 63 }

 

5.該類的全部程式碼

記一次 Java 匯出大批量 Excel 優化
  1 package mytest;
  2 
  3 import bean.ExcelBean;
  4 import com.alibaba.excel.EasyExcel;
  5 import com.alibaba.excel.ExcelWriter;
  6 import com.alibaba.excel.write.metadata.WriteSheet;
  7 import com.google.common.collect.Lists;
  8 import lombok.extern.slf4j.Slf4j;
  9 import org.junit.Test;
 10 
 11 import java.util.Date;
 12 import java.util.List;
 13 import java.util.UUID;
 14 
 15 /**
 16  * @author dengzeng
 17  * @date 2021-11-06 上午 8:57
 18  * 
 19  */
 20 @Slf4j
 21 public class TestExcel {
 22     public static final String FILE_NAME = "D:\\test_" + System.currentTimeMillis() + ".xlsx";
 23     // 每個 sheet 寫入的資料
 24     public static final int NUM_PER_SHEET = 300000;
 25     // 每次向 sheet 中寫入的資料(分頁寫入)
 26     public static final int NUM_BY_TIMES = 50000;
 27 
 28     /**
 29      * 方法一:將資料寫入到excel
 30      * 直接呼叫api,適合小資料量
 31      * 100W條資料33s
 32      */
 33     @Test
 34     public void writeExcelByApi() {
 35         String fileName = FILE_NAME;
 36         log.info("匯出excel名稱={}", fileName);
 37         long startTime = System.currentTimeMillis();
 38         // 直接呼叫api
 39         List<ExcelBean> date = getDate();
 40         EasyExcel.write(fileName, ExcelBean.class).sheet().doWrite(date);
 41         log.info("匯出excel結束,資料量={},耗時={}ms", date.size(), System.currentTimeMillis() - startTime);
 42     }
 43 
 44     /**
 45      * 方法二:匯出多個sheet
 46      * easyExcel 底層是 POI 實現的,POI 單個sheet 最多隻能匯出 1048576 行,超過該行數,會產生如下異常
 47      * java.lang.IllegalArgumentException: Invalid row number (1048576) outside allowable range (0..1048575)
 48      * <p>
 49      * 11:57:55.541 [main] INFO mytest.TestExcel - 寫入sheet=sheet0,資料量300000-0=300000,耗時=6055ms
 50      * 11:57:59.701 [main] INFO mytest.TestExcel - 寫入sheet=sheet1,資料量600000-300000=300000,耗時=4159ms
 51      * 11:58:03.827 [main] INFO mytest.TestExcel - 寫入sheet=sheet2,資料量900000-600000=300000,耗時=4126ms
 52      * 11:58:05.193 [main] INFO mytest.TestExcel - 寫入sheet=sheet3,資料量1000000-900000=100000,耗時=1366ms
 53      * 11:58:17.418 [main] INFO mytest.TestExcel - 匯出excel結束,總資料量=1000000,耗時=31297ms
 54      */
 55     @Test
 56     public void writeExcelByMulSheet() {
 57         String fileName = FILE_NAME;
 58         log.info("匯出excel名稱={}", fileName);
 59         long startTime = System.currentTimeMillis();
 60         // 獲取資料
 61         List<ExcelBean> date = getDate();
 62         // 獲取 sheet 的個數
 63         int sheetNum = date.size() % NUM_PER_SHEET == 0 ? date.size() / NUM_PER_SHEET : date.size() / NUM_PER_SHEET + 1;
 64         // 指定寫入的檔案
 65         ExcelWriter excelWriter = EasyExcel.write(fileName, ExcelBean.class).build();
 66         for (int i = 0; i < sheetNum; i++) {
 67             long l = System.currentTimeMillis();
 68             // 設定 sheet 的名字(sheet不能相同)
 69             String sheetName = "sheet" + i;
 70             WriteSheet writeSheet = EasyExcel.writerSheet(i, sheetName).build();
 71             int startNum = i * NUM_PER_SHEET;
 72             int endNum = i == sheetNum - 1 ? date.size() : (i + 1) * NUM_PER_SHEET;
 73             excelWriter.write(date.subList(startNum, endNum), writeSheet);
 74             log.info("寫入sheet={},資料量{}-{}={},耗時={}ms", sheetName, endNum, startNum, endNum - startNum, System.currentTimeMillis() - l);
 75         }
 76         // 最好放在 finally中
 77         excelWriter.finish();
 78         log.info("匯出excel結束,總資料量={},耗時={}ms", date.size(), System.currentTimeMillis() - startTime);
 79     }
 80 
 81     /**
 82      * 方法三:同一個 Sheet,分批多次寫入
 83      * 當單次讀取的 list 資料量過大,會產生 OOM 異常,所以需要分頁讀取並寫入到 excel
 84      * 11:55:01.590 [main] INFO mytest.TestExcel - 寫入數量50000-0=50000,耗時=2227ms
 85      * 11:55:02.429 [main] INFO mytest.TestExcel - 寫入數量100000-50000=50000,耗時=838ms
 86      * 11:55:03.188 [main] INFO mytest.TestExcel - 寫入數量150000-100000=50000,耗時=759ms
 87      * 11:55:03.951 [main] INFO mytest.TestExcel - 寫入數量200000-150000=50000,耗時=762ms
 88      * 11:55:04.708 [main] INFO mytest.TestExcel - 寫入數量250000-200000=50000,耗時=757ms
 89      * 11:55:05.471 [main] INFO mytest.TestExcel - 寫入數量300000-250000=50000,耗時=762ms
 90      * 11:55:06.172 [main] INFO mytest.TestExcel - 寫入數量350000-300000=50000,耗時=701ms
 91      * 11:55:06.921 [main] INFO mytest.TestExcel - 寫入數量400000-350000=50000,耗時=749ms
 92      * 11:55:07.688 [main] INFO mytest.TestExcel - 寫入數量450000-400000=50000,耗時=767ms
 93      * 11:55:08.437 [main] INFO mytest.TestExcel - 寫入數量500000-450000=50000,耗時=749ms
 94      * 11:55:09.141 [main] INFO mytest.TestExcel - 寫入數量550000-500000=50000,耗時=704ms
 95      * 11:55:09.899 [main] INFO mytest.TestExcel - 寫入數量600000-550000=50000,耗時=758ms
 96      * 11:55:10.597 [main] INFO mytest.TestExcel - 寫入數量650000-600000=50000,耗時=698ms
 97      * 11:55:11.353 [main] INFO mytest.TestExcel - 寫入數量700000-650000=50000,耗時=756ms
 98      * 11:55:12.055 [main] INFO mytest.TestExcel - 寫入數量750000-700000=50000,耗時=701ms
 99      * 11:55:12.820 [main] INFO mytest.TestExcel - 寫入數量800000-750000=50000,耗時=765ms
100      * 11:55:13.576 [main] INFO mytest.TestExcel - 寫入數量850000-800000=50000,耗時=756ms
101      * 11:55:14.287 [main] INFO mytest.TestExcel - 寫入數量900000-850000=50000,耗時=711ms
102      * 11:55:15.055 [main] INFO mytest.TestExcel - 寫入數量950000-900000=50000,耗時=768ms
103      * 11:55:15.773 [main] INFO mytest.TestExcel - 寫入數量1000000-950000=50000,耗時=718ms
104      * 11:55:28.016 [main] INFO mytest.TestExcel - 匯出excel結束,總資料量=1000000,耗時=31738ms
105      * <p>
106      * Process finished with exit code 0
107      */
108     @Test
109     public void writeExcelByMulWrite() {
110         String fileName = FILE_NAME;
111         log.info("匯出excel名稱={}", fileName);
112         long startTime = System.currentTimeMillis();
113         // 獲取資料
114         List<ExcelBean> date = getDate();
115         ExcelWriter excelWrite = EasyExcel.write(fileName, ExcelBean.class).build();
116         WriteSheet writeSheet = EasyExcel.writerSheet("testSheet").build();
117         // 計算需要寫入的次數
118         int times = date.size() % NUM_BY_TIMES == 0 ? date.size() / NUM_BY_TIMES : date.size() / NUM_BY_TIMES + 1;
119         for (int i = 0; i < times; i++) {
120             long l = System.currentTimeMillis();
121             int startNum = i * NUM_BY_TIMES;
122             int endNum = i == times - 1 ? date.size() : (i + 1) * NUM_BY_TIMES;
123             excelWrite.write(date.subList(startNum, endNum), writeSheet);
124             log.info("寫入數量{}-{}={},耗時={}ms", endNum, startNum, endNum - startNum, System.currentTimeMillis() - l);
125         }
126         // 需要放入 finally 中
127         if (excelWrite != null) {
128             excelWrite.finish();
129         }
130         log.info("匯出excel結束,總資料量={},耗時={}ms", date.size(), System.currentTimeMillis() - startTime);
131     }
132     
133 
134     /**
135      * 方案四:寫入多個 sheet,並且每個 sheet 寫入多次資料(結合方案二、三)
136      * 資料量大,導致一個 sheet 儲存不下;同時單次讀入的資料量太大。可以採用這個方法
137      * 12:02:18.751 [main] INFO mytest.TestExcel - 寫入sheet=sheet0,資料量=50000-0=50000,耗時=1558
138      * 12:02:19.542 [main] INFO mytest.TestExcel - 寫入sheet=sheet0,資料量=100000-50000=50000,耗時=791
139      * 12:02:20.282 [main] INFO mytest.TestExcel - 寫入sheet=sheet0,資料量=150000-100000=50000,耗時=740
140      * 12:02:21.037 [main] INFO mytest.TestExcel - 寫入sheet=sheet0,資料量=200000-150000=50000,耗時=755
141      * 12:02:21.781 [main] INFO mytest.TestExcel - 寫入sheet=sheet0,資料量=250000-200000=50000,耗時=744
142      * 12:02:22.524 [main] INFO mytest.TestExcel - 寫入sheet=sheet0,資料量=300000-250000=50000,耗時=742
143      * 12:02:23.201 [main] INFO mytest.TestExcel - 寫入sheet=sheet1,資料量=350000-300000=50000,耗時=677
144      * 12:02:23.852 [main] INFO mytest.TestExcel - 寫入sheet=sheet1,資料量=400000-350000=50000,耗時=651
145      * 12:02:24.451 [main] INFO mytest.TestExcel - 寫入sheet=sheet1,資料量=450000-400000=50000,耗時=599
146      * 12:02:25.100 [main] INFO mytest.TestExcel - 寫入sheet=sheet1,資料量=500000-450000=50000,耗時=649
147      * 12:02:25.753 [main] INFO mytest.TestExcel - 寫入sheet=sheet1,資料量=550000-500000=50000,耗時=653
148      * 12:02:26.350 [main] INFO mytest.TestExcel - 寫入sheet=sheet1,資料量=600000-550000=50000,耗時=597
149      * 12:02:26.995 [main] INFO mytest.TestExcel - 寫入sheet=sheet2,資料量=650000-600000=50000,耗時=645
150      * 12:02:27.588 [main] INFO mytest.TestExcel - 寫入sheet=sheet2,資料量=700000-650000=50000,耗時=593
151      * 12:02:28.244 [main] INFO mytest.TestExcel - 寫入sheet=sheet2,資料量=750000-700000=50000,耗時=656
152      * 12:02:28.893 [main] INFO mytest.TestExcel - 寫入sheet=sheet2,資料量=800000-750000=50000,耗時=648
153      * 12:02:29.506 [main] INFO mytest.TestExcel - 寫入sheet=sheet2,資料量=850000-800000=50000,耗時=613
154      * 12:02:30.163 [main] INFO mytest.TestExcel - 寫入sheet=sheet2,資料量=900000-850000=50000,耗時=657
155      * 12:02:30.760 [main] INFO mytest.TestExcel - 寫入sheet=sheet3,資料量=950000-900000=50000,耗時=597
156      * 12:02:31.419 [main] INFO mytest.TestExcel - 寫入sheet=sheet3,資料量=1000000-950000=50000,耗時=659
157      * 12:02:43.235 [main] INFO mytest.TestExcel - 匯出excel結束,總資料量=1000000,耗時=28818ms
158      *
159      * Process finished with exit code 0
160      */
161     @Test
162     public void writeExcelByMulSheetAndWriteChange() {
163         String fileName = FILE_NAME;
164         log.info("匯出excel名稱={}", fileName);
165         long startTime = System.currentTimeMillis();
166         // 獲取資料
167         List<ExcelBean> date = getDate();
168         // 獲取 sheet 的個數
169         int sheetNum = date.size() % NUM_PER_SHEET == 0 ? date.size() / NUM_PER_SHEET : date.size() / NUM_PER_SHEET + 1;
170         // 獲取每個sheet 寫入的次數
171         int writeNumPerSheet = NUM_PER_SHEET % NUM_BY_TIMES == 0 ? NUM_PER_SHEET / NUM_BY_TIMES : NUM_PER_SHEET / NUM_BY_TIMES + 1;
172         // 最後一個 sheet 寫入的數量
173         int writeNumLastSheet = date.size() - (sheetNum - 1) * NUM_PER_SHEET;
174         // 最後一個 sheet 寫入的次數
175         int writeNumPerLastSheet = writeNumLastSheet % NUM_BY_TIMES == 0 ? writeNumLastSheet / NUM_BY_TIMES : writeNumLastSheet / NUM_BY_TIMES + 1;
176         // 指定寫入的檔案
177         ExcelWriter excelWriter = EasyExcel.write(fileName, ExcelBean.class).build();
178         for (int i = 0; i < sheetNum; i++) {
179             String sheetName = "sheet" + i;
180             WriteSheet writeSheet = EasyExcel.writerSheet(i, sheetName).build();
181             int writeNum = i == sheetNum - 1 ? writeNumPerLastSheet : writeNumPerSheet; // 每個sheet 寫入的次數
182             int endEndNum = i == sheetNum - 1 ? date.size() : (i + 1) * NUM_PER_SHEET; // 每個sheet 最後一次寫入的最後行數
183             for (int j = 0; j < writeNum; j++) {
184                 long l = System.currentTimeMillis();
185                 int startNum = i * NUM_PER_SHEET + j * NUM_BY_TIMES;
186                 int endNum = j == writeNum - 1 ? endEndNum : i * NUM_PER_SHEET + (j + 1) * NUM_BY_TIMES;
187                 excelWriter.write(date.subList(startNum, endNum), writeSheet);
188                 log.info("寫入sheet={},資料量={}-{}={},耗時={}", sheetName, endNum, startNum, endNum - startNum, System.currentTimeMillis() - l);
189             }
190         }
191         // 需要放入 finally 中
192         if (excelWriter != null) {
193             excelWriter.finish();
194         }
195         log.info("匯出excel結束,總資料量={},耗時={}ms", date.size(), System.currentTimeMillis() - startTime);
196     }
197 
198 
199     /**
200      * 獲取excel 匯出的資料
201      *
202      * @return list 集合
203      */
204     private List<ExcelBean> getDate() {
205         log.info("開始生成資料");
206         Date date = new Date();
207         long startTime = System.currentTimeMillis();
208         List<ExcelBean> list = Lists.newArrayList();
209         for (int i = 0; i < 1000000; i++) {
210             ExcelBean bean = new ExcelBean();
211             bean.setId(UUID.randomUUID().toString()).
212                     setName("隔壁老樊" + i).
213                     setAddress("北京市朝陽區酒仙橋" + i + "路").
214                     setAge(i).
215                     setNumber(i + 10000).
216                     setHigh(1.234 * i).
217                     setDistance(1.234 * i).
218                     setStartTime(date).
219                     setEndTime(date);
220             list.add(bean);
221         }
222         log.info("資料生成結束,資料量={},耗時={}ms", list.size(), System.currentTimeMillis() - startTime);
223         return list;
224     }
225 }
View Code