前言
眾所周知,導Excel分為兩步:
- 抓取數據(查數據)
- 寫數據到Excel文件
這兩步都比較耗時間,一般我們從數據庫查數據,然后組裝數據,最后寫數據。
查數據不是本節的重點,主要是SQL,索引這一塊,此處不討論。本節重點是寫數據。
問題
當數據量小(比如,幾千幾萬條)的時候可以采用同步的方式,不用考慮別的。
而當數據量大的時候(比如,幾十上百萬)的時候問題就暴露出來了。
首先,慢是肯定的了。少則幾十秒,多則幾十分鍾都是有可能的。
這還是小問題,最要命的因為一個導出把系統搞掛了。。。
筆者曾經見過,因為一個導出,系統直接掛了,還嚴重拖慢了同一台機器上的其它應用,最終宕機了。。。
究其原因,大量數據堆積在內存中,可能會造成內存溢出。誇張一點,幾百萬條數據每條數據幾十個字段都放到內存中,要等到全部寫完這些內存才會釋放。
方案
- 針對單個工作表(sheet)的行數限制,可以分多個工作表
- 針對單個文件太大不容易打開,可以分多個文件,最終打成壓縮包
- 針對內存溢出,可以分批導,每次導一批數據,分多次導
建議
- 異步下載!異步!異步!異步!
- 如果對樣式沒什么要求,也不用公式的話,強烈推薦導出CSV格式
- 可以采用多線程的方式,先查總數,然后分一下看需要多少個線程,每個線程讀取一部數據並寫入單獨Excel文件;當然,也可以多線程讀,單線程寫
- 分批導,這一點跟上一步類似
思路
客戶端發起下載請求以后,服務端異步執行下載任務並生成下載文件,客戶端讀取這個文件下載。
那么問題來了,客戶端怎么知道服務端下載文件已經生成好了呢?
有一個方案是: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>
參考
https://www.cnblogs.com/cjsblog/p/9260421.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
https://www.cnblogs.com/gossip/p/5795333.html
https://blog.csdn.net/happyljw/article/details/52809244

