新一代 Excel 匯出工具:ExcelUtil + RunnerUtil 介紹

蜜汁微笑發表於2018-11-28

背景 —— 提一個好的問題

開發過程中經常會遇到 Excel 匯出的情況,尤其是在企業開發中,涉及到客戶資訊、財務報表、市場分析等,情景非常多。平常開發過程中大多都會針對每個匯出單獨寫一套程式碼,隨著匯出越來越多,心裡便想:有沒有一個足夠通用東西可以讓我們不用寫這麼多程式碼來實現 Excel 匯出?

帶著這個問題便開始了自己的“ExcelUtil”之路,在這過程中主要接觸過 easypoi,但還是不太滿足。因為 easypoi 和大多數 Java 庫一樣:基於欄位寫配置。當然不是說這個不好,有很多庫都這樣,比如 fastjson、Jackson 等都是在欄位上寫註解,描述這個欄位有些什麼資訊或作用等。但對於 Excel 匯出,我總覺得還有更加通用的方式。

經過一段時間的摸索和發掘,在前端的 table 標籤上找到了靈感,認為這個方式很好、非常好。table 標籤本身包含了很多描述資訊,像行、列、合併行、合併列這些與 excel 的 sheet 頁“驚人的相似”,再加上近幾年前端三大框架的大力發展,尤其是 angular 和 vue 這兩個框架在標籤上自定義屬性的方式進一步讓我在寫 ExcelUtil 過程中得到了不少啟發。

簡介

ExcelUtil 和 RunnerUtilGitHub) 一樣,大概是在今年 5 到 6 月寫的,最近又重新整理了一下,已上傳 GitHub # ExcelUtil (記得 star 哈!)

ExcelUtil 根據 excel 檔案、sheet 頁、row 行、cell 單元格這樣的層次結構分別定義了自己的作用域,每個作用域內可以一定程度上自定義變數等,作用域之間互不影響,同名變數下層作用域等宣告優先於上層作用域等這些與 java、JavaScript 等語言的作用域結構一致。

不同的是 ExcelUtil 使用頻率比 RunnerUtil 頻率高很多,寫 RunnerUtil 的初衷也是為了這個 ExcelUtil 匯出,最開始想到了 Java 內建指令碼引擎(ScriptEngine),但內建指令碼引擎的效率實在太低,資料量稍微大一點(不用太大)情況下直接卡死(不該這麼吐槽的,但的確不適合這個場景)。但是 RunnerUtil 的功能獨立且完善,效能良好,可以執行各種複雜的 Java 字串公式,完全可以單獨使用。

使用介紹

  1. 使用 ExcelUtil 的之前首先要準備的就是資料,資料並沒有特殊的格式要求,可以是任意 Java 型別資料,如 Collection、Iterable、Iterator(迭代器模式可,這是在一次面試時得到的啟發,可用於超大 Excel 匯出,雖然後來沒通過,但仍然很感謝那位面試官!)、Map、陣列、POJO、Number等。

  2. 第二步是生成 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 (或冒號 :)可迭代的資料有:
  1. number(整數),如 var = "$item in 10",迴圈十次;
  2. 字串,迭代出字串中的每個字元,但由於 RunnerUtil 是不支援 char 型別資料的,所以實際上迭代出來的是單個字元的字串
  3. Collection、Iterable、List、Set 等集合。
  4. Map,迭代出來的是每一個鍵值對的值;
  5. POJO,普通 Java 物件按欄位名迭代,迭代出的是欄位值
  • when 後面的表示式返回值必須是 boolean 型別
  • colspan、rowspan 表示式返回值必須是 int 型別
  • 其他的還有 heigit、width 等也必須是 int 型別

使用效果:

https://user-gold-cdn.xitu.io/2018/11/28/16758316e9699b4d?w=1457&h=813&f=png&s=254543

  • 生成的對應 Excel 效果圖

https://user-gold-cdn.xitu.io/2018/11/28/16758316e9782ec1?w=649&h=309&f=png&s=12588

效能測試

貼一個本工具匯出的 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 萬行基本是正常業務極限值了。
  • 目前只支援匯出,還不能匯入。

ExcelUtil # GitHub # star ~!

RunnerUtil 的用法介紹

相關文章