背景 —— 提一個好的問題
開發過程中經常會遇到 Excel 匯出的情況,尤其是在企業開發中,涉及到客戶資訊、財務報表、市場分析等,情景非常多。平常開發過程中大多都會針對每個匯出單獨寫一套程式碼,隨著匯出越來越多,心裡便想:有沒有一個足夠通用東西可以讓我們不用寫這麼多程式碼來實現 Excel 匯出?
帶著這個問題便開始了自己的“ExcelUtil”之路,在這過程中主要接觸過 easypoi,但還是不太滿足。因為 easypoi 和大多數 Java 庫一樣:基於欄位寫配置。當然不是說這個不好,有很多庫都這樣,比如 fastjson、Jackson 等都是在欄位上寫註解,描述這個欄位有些什麼資訊或作用等。但對於 Excel 匯出,我總覺得還有更加通用的方式。
經過一段時間的摸索和發掘,在前端的 table 標籤上找到了靈感,認為這個方式很好、非常好。table 標籤本身包含了很多描述資訊,像行、列、合併行、合併列這些與 excel 的 sheet 頁“驚人的相似”,再加上近幾年前端三大框架的大力發展,尤其是 angular 和 vue 這兩個框架在標籤上自定義屬性的方式進一步讓我在寫 ExcelUtil 過程中得到了不少啟發。
簡介
ExcelUtil 和 RunnerUtil(GitHub) 一樣,大概是在今年 5 到 6 月寫的,最近又重新整理了一下,已上傳 GitHub # ExcelUtil (記得 star 哈!)。
ExcelUtil 根據 excel 檔案、sheet 頁、row 行、cell 單元格這樣的層次結構分別定義了自己的作用域,每個作用域內可以一定程度上自定義變數等,作用域之間互不影響,同名變數下層作用域等宣告優先於上層作用域等這些與 java、JavaScript 等語言的作用域結構一致。
不同的是 ExcelUtil 使用頻率比 RunnerUtil 頻率高很多,寫 RunnerUtil 的初衷也是為了這個 ExcelUtil 匯出,最開始想到了 Java 內建指令碼引擎(ScriptEngine),但內建指令碼引擎的效率實在太低,資料量稍微大一點(不用太大)情況下直接卡死(不該這麼吐槽的,但的確不適合這個場景)。但是 RunnerUtil 的功能獨立且完善,效能良好,可以執行各種複雜的 Java 字串公式,完全可以單獨使用。
使用介紹
-
使用 ExcelUtil 的之前首先要準備的就是資料,資料並沒有特殊的格式要求,可以是任意 Java 型別資料,如 Collection、Iterable、Iterator(迭代器模式可,這是在一次面試時得到的啟發,可用於超大 Excel 匯出,雖然後來沒通過,但仍然很感謝那位面試官!)、Map、陣列、POJO、Number等。
-
第二步是生成 Workbook 位置的方法上進行“註解程式設計” —— 對的,Java 的註解功能很強大,可以在 Java 內部又單獨作為 Java 內的“程式語言”(其實就是寫了個簡單的解析器而言,捂臉一笑)。
// 在什麼地方匯出,就在那個方法上進行宣告式“註解程式設計”
// 首先要宣告這是一個 Excel,用 type 指定是 xls 或者 xlsx
@TableExcel(type = TableExcel.Type.XLS, value = {
/*
* value 包含的是所有 sheet 頁的資訊
* 自 sheet 向下,每個標籤可以判斷、迴圈等
* 用 sheetName 指定 sheet 名
* 為什麼要用單引號再多包裹一層呢?詳見 RunnerUtil
* 因為這裡面的所有內容都是用 RunnerUtil 解析的,需要符合它的格式
*/
@TableSheet(sheetName = "'人員資訊'", value = {
/*
* 在這兒宣告瞭一個名為 names 的陣列,用作標題
*/
@TableRow(var = "names = {'序號','姓名','性別','年齡','電話','家庭住址', '備註'}", value = {
/*
* 這兒用了迭代,迭代 row 上宣告的 names
* 這個迭代將按 names 的內容生成對應數量和內容的 cell 單元格
*/
@TableCell(var = "name:names", value = name)
}),
/*
* 上面 cell 的迭代用的是冒號,這兒用了 in,二者意義完全一樣
* 支援 in 完全是為了向靈感的來源(前端)致敬
* 但是 in 並不是關鍵字,仍可作為普通變數
* 不同的是 in 的兩端至少各有一個空格
* 可迭代的資料型別一會兒詳細介紹
*/
@TableRow(var = "($rowData, index) in collect", value = {
@TableCell("index + 1"), // 序號
@TableCell("$rowData.name"), // 姓名
@TableCell("$rowData.sex"), // 性別
@TableCell("$rowData.age"), // 年齡
@TableCell("$rowData.mobile"), // 電話
@TableCell("$rowData.address"), // 家庭住址
// 最後這個對於上面的備註,這兒有個 when,只有 index == 0 才建立這個單元格
// 同時這兒還用到了併合並行,另外 colspan 是合併列
@TableCell(when = "index == 0", rowspan = "data.size()")
})
})
})
public Workbook exportExcel(Object data){
/*
* 寫好註解後只需要呼叫這個方法便可得到一個 Workbook
* 在哪兒呼叫 render 方法就在哪兒寫上面那些註解
*/
return ExcelUtil.render(data);
}
複製程式碼
- ExcelUtil.render(data); 在渲染中 in (或冒號 :)可迭代的資料有:
- number(整數),如 var = "$item in 10",迴圈十次;
- 字串,迭代出字串中的每個字元,但由於 RunnerUtil 是不支援 char 型別資料的,所以實際上迭代出來的是單個字元的字串
- Collection、Iterable、List、Set 等集合。
- Map,迭代出來的是每一個鍵值對的值;
- POJO,普通 Java 物件按欄位名迭代,迭代出的是欄位值
- when 後面的表示式返回值必須是 boolean 型別
- colspan、rowspan 表示式返回值必須是 int 型別
- 其他的還有 heigit、width 等也必須是 int 型別
使用效果:
- 生成的對應 Excel 效果圖
效能測試
貼一個本工具匯出的 10 列 Excel 的效能測試表(本機環境 i7-8700K 16G Win10)
行數(萬行) | 生成資料耗時(ms) | write到檔案耗時(ms) | 總耗時(ms) |
---|---|---|---|
100 | 6,182 | 5,565 | 11,747 |
300 | 14,800 | 16,693 | 31,493 |
500 | 25,876 | 27,317 | 53,193 |
700 | 36,121 | 42,171 | 78,292 |
999 | 53,532 | 54,745 | 108,277 |
4000 | 240,453 | 271,832 | 512,285 |
6000 | 366,987 | 423,351 | 790,338 |
8000 | 528,654 | 498,490 | 1,027,144 |
從這個資料可以看出,隨著資料量增加,時間與資料的關係呈正相關性,接近線性關係,100 萬行資料生成 Workbook 耗時 6s,總耗時 12s,在正常業務場景下能滿足時間的要求。
其他說明
- 當 Excel 資料量超過 150 萬行時,不建議用 xls 格式(這個資料在不同機器上應該有差異,本機 150 萬行的 xls 能正常匯出,180 萬行就 OOM 了);
- 當資料量超過 500 萬行時(隨環境而異),TableExcel 的 type 值應為 SUPER(type = TableExcel.Type.SUPER),SUPER 對應的也是 xlsx 格式,但是 SUPER 是用來支援超大資料匯出的;
- 150 萬行和 500 萬行基本是正常業務極限值了。
- 目前只支援匯出,還不能匯入。