點擊這里查看原文
本系列主要介紹Easyexcel和EEC的功能并從便利性尚猿、性能、內(nèi)存等多方面全面的進行評測。
1. 開始
關(guān)于Easyexcel
easyexcel是alibaba開發(fā)的快速奕谭、簡單涣觉、且避免OOM的java處理Excel工具,于2018.2在github上開源血柳。它是在Apache POI基礎(chǔ)上包裝而來官册,主要解決Apache POI高內(nèi)存且API臃腫的詬病,easyexcel提供了比原生的POI簡結(jié)很多的接口混驰,讀寫Excel文件均可以一行代碼完成攀隔,目前(2020.4)github上有14.2k個Star和3.7K個Fork。
引用作者總結(jié)核心原理:
- 文件解壓栖榨、讀取通過文件形式
- 避免將全部數(shù)據(jù)一次加載到內(nèi)存(采用sax模式一行一行解析并使用觀察者的模式通知處理)
- 拋棄不重要的數(shù)據(jù)(忽略樣式昆汹,字體,寬度等數(shù)據(jù))
點擊這里查看作者原文
關(guān)于EEC
EEC是國內(nèi)一個個人開發(fā)者開發(fā)并于2017.10月在github開源婴栽,EEC的底層并沒有使用Apache POI包满粗,所有的底層讀寫代碼均由作者實現(xiàn),事實上EEC僅依懶dom4j和slf4j愚争,前者用于小文件xml讀取映皆,后者統(tǒng)一日志接口。
核心原理:
- 不緩存數(shù)據(jù)或少量緩存
- 使用分片來處理較大的數(shù)據(jù)
- 單元格樣式僅使用一個int值來保存轰枝,極大縮小內(nèi)存使用
- 使用迭代模式讀取行內(nèi)容捅彻,不會將整個文件讀入到內(nèi)存
簡單總結(jié)兩個工具的不同:
- 底層不同,easyexcel底層使用Apache POI鞍陨,EEC使用IO/NIO
- easyexcel最低支持JDK7步淹,EEC最低支持JDK8
- easyexcel簡化了接口使得像設(shè)置樣式這種基本功能非常困難,EEC默認帶有便于閱讀的樣式也提供方法設(shè)置其它樣式
- easyexcel讀取文件時忽略樣式和字體也沒有辦法直接獲取單元格的公式诚撵。
- easyexcel對常用類型缺少支持(char, Timestamp, Time, LocalDate, LocalDateTime, LocalTime)缭裆,如果實體類中有這些類型就必須為這些類型編寫自定義Converter
相比之下EEC更接近于Apache POI,而easyexcel更關(guān)注單元格的值而忽略其它不太關(guān)心的數(shù)據(jù)寿烟。
2. 寫文件
2.1 少量數(shù)據(jù)
對于少量數(shù)據(jù)可以直接將內(nèi)容放到數(shù)組/集合中一次寫入澈驼,兩個工具都能做到一行代碼完成數(shù)據(jù)寫入。下面展示兩者的實現(xiàn)方式筛武,代碼中出現(xiàn)的defaultTestPath
是文件路徑事先已創(chuàng)建好缝其。
easyexcel可以將文件直接寫入OutputStream
或磁盤
public void test5(List<Item> data) {
EasyExcel.write(defaultTestPath.resolve("test5.xlsx").toString(), LargeData.class).sheet().doWrite(data);
}
EEC同樣可以寫入OutputStream
或磁盤,使用writeTo
方法指定輸出位置
public void test6(List<Item> data) throws IOException {
new Workbook("test6").addSheet(new ListSheet<>(data)).writeTo(defaultTestPath);
}
2.2 寫多個worksheet頁
兩個工具都提供便利的方法實現(xiàn)多worksheet頁寫入徘六,基本可以使用一行代碼搞定内边。
easyexcel通過創(chuàng)建多個WriteSheet來實現(xiàn)
public void test7() {
EasyExcel.write(defaultTestPath.resolve("test7.xlsx").toString()).build()
.write(checks(), EasyExcel.writerSheet("帳單表").build())
.write(customers(), EasyExcel.writerSheet("客戶表").build())
.write(c2CS(), EasyExcel.writerSheet("用戶客戶關(guān)系表").build())
.finish();
}
EEC通過addSheet
方法添加多個worksheet,看上去更直觀更容易理解硕噩。
public void test8() throws IOException {
new Workbook("test8")
.addSheet(new ListSheet<>("帳單表", checks()))
.addSheet(new ListSheet<>("客戶表", customers()))
.addSheet(new ListSheet<>("用戶客戶關(guān)系表", c2CS()))
.writeTo(defaultTestPath);
}
2.3 大數(shù)據(jù)量
數(shù)據(jù)量較大時我們無法將數(shù)據(jù)全部裝載到內(nèi)存假残,此時需要分批寫文件缭贡,好在easyexcel和EEC均支持分片處理做到邊讀數(shù)據(jù)邊寫文件炉擅。
easyexcel寫大文件需要指定一個模板文件辉懒,然后調(diào)用fill
方法循環(huán)寫數(shù)據(jù),需要注意如果數(shù)據(jù)量超出excel單頁上限會拋異常
public void test1() {
ExcelWriter excelWriter = EasyExcel.write(defaultTestPath.resolve("Large easyexcel.xlsx").toFile())
.withTemplate(defaultTestPath.resolve("temp.xlsx").toFile()).build();
WriteSheet writeSheet = EasyExcel.writerSheet().build();
for (int j = 0; j < 100; j++) {
excelWriter.fill(data(), writeSheet);
}
excelWriter.finish();
}
EEC分片寫大文件時需要繼承ListSheet<T>
或ListMapSheet
然后重寫more
方法并返回批量數(shù)據(jù)
public void test2() {
new Workbook("Large EEC").addSheet(new ListSheet<LargeData>() {
int n = 0;
@Override
public List<LargeData> more() {
return n++ < 100 ? data() : null;
}
}).writeTo(defaultTestPath);
}
這里的data()方法模擬取數(shù)據(jù)過程谍失,返回List<LargeData>
類型眶俩。類似如下代碼
private List<LargeData> data() {
List<LargeData> list = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
LargeData largeData = new LargeData();
list.add(largeData);
largeData.setStr1("str1-" + i);
largeData.setStr2("str2-" + i);
largeData.setStr3("str3-" + i);
largeData.setStr4("str4-" + i);
largeData.setStr5("str5-" + i);
}
return list
}
以上是兩個工具類處理大文件的不同方式,easyexcel采用push方式主動向ExcelWriter推送數(shù)據(jù)快鱼,EEC采用pull方式由工具決定何時拉取下一塊數(shù)據(jù)颠印,返回空數(shù)組或null時表明沒有更多數(shù)據(jù),所以這里要注意控制分頁參數(shù)防止出現(xiàn)死循環(huán)抹竹。
對于數(shù)據(jù)量巨大且使用關(guān)系型數(shù)據(jù)庫的場景线罕,EEC提供另一種方案,用戶可以使用StatementSheet
和ResultSetSheet
兩種方式窃判,它們的工作方式是將SQL和參數(shù)交給EEC钞楼,EEC內(nèi)部去查詢并使用游標做到取一個值寫一個值,省掉了將表數(shù)據(jù)轉(zhuǎn)為Java實體的過程袄琳。
3. 讀文件
easyexcel在讀文件時使用ReadListener
來監(jiān)聽每行數(shù)據(jù)询件,這樣可以做到邊解析文件邊做業(yè)務(wù)邏輯(插庫或其它),不用把文件解析完成后再做業(yè)務(wù)邏輯唆樊,以下是解析圖示:
EEC采用迭代模式宛琅,同樣做到邊解析文件邊做業(yè)務(wù)邏輯,解決POI的高內(nèi)存問題逗旁。
從兩者圖示大致可以看出兩者的設(shè)計與寫文件時正好相反嘿辟。easyexcel通過監(jiān)聽主動把行數(shù)據(jù)推給用戶,EEC這邊需要用戶主動拉數(shù)據(jù)痢艺,只有當用戶真正需要某行數(shù)據(jù)時才去解析它們來實現(xiàn)延遲讀取仓洼。
3.1 easyexcel讀文件
public void test3() {
EasyExcel.read(defaultTestPath.resolve("Large easyexcel.xlsx").toFile(), LargeData.class,
new AnalysisEventListener<LargeData>() {
@Override
public void invoke(LargeData data, AnalysisContext context) {
// 業(yè)務(wù)處理
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) { }
}).headRowNumber(1).sheet().doRead();
}
你需要實現(xiàn)一個ReaderListener
來處理行數(shù)據(jù)。
3.2 EEC讀文件
try (ExcelReader reader = ExcelReader.read(defaultTestPath.resolve("Large easyexcel.xlsx"))) {
reader.sheet("帳單表") // 解析指定worksheet
.flatMap(Sheet::dataRows) // 只取數(shù)據(jù)行堤舒,跳過表頭
.map(row -> row.to(LargeData.class)) // 轉(zhuǎn)為實體對象
.forEach(o -> {
// 業(yè)務(wù)處理
});
} catch (IOException e) {
e.printStackTrace();
}
EEC引入java8的stream+lambda功能色建,你可以像操作集合類一樣來操作Excel,而不用擔心OOM發(fā)生舌缤。
3.3 讀取多個worksheet頁
兩個工具都提供方便的多worksheet讀取箕戳,可以看示例
easyexcel示例
public void test9() {
ExcelReader excelReader = EasyExcel.read(defaultTestPath.resolve("test7.xlsx").toFile(), simpleListener).headRowNumber(0).build();
List<ReadSheet> sheets = excelReader.excelExecutor().sheetList();
sheets.forEach(sheet -> {
System.out.println("----------" + sheet.getSheetName() + "-----------");
excelReader.read(sheet);
});
}
// 輸出內(nèi)容
----------帳單表-----------
{0=1.0, 1=100.8}
{0=2.0, 1=34.2}
{0=3.0, 1=983.0}
----------客戶表-----------
{0=1001.0, 1=張三}
{0=1002.0, 1=李四}
----------用戶客戶關(guān)系表-----------
{0=1.0, 1=1001.0}
{0=2.0, 1=1002.0}
{0=3.0, 1=1002.0}
EEC示例
public void test10() {
try (ExcelReader reader = ExcelReader.read(defaultTestPath.resolve("test8.xlsx"))) {
reader.sheets()
.peek(sheet -> System.out.println("----------" + sheet.getName() + "-----------"))
.flatMap(Sheet::rows)
.forEach(System.out::println);
} catch (IOException e) {
e.printStackTrace();
}
}
// 輸出內(nèi)容
----------帳單表-----------
id | total
1 | 100.8
2 | 34.2
3 | 983
----------客戶表-----------
id | name
1001 | 張三
1002 | 李四
----------用戶客戶關(guān)系表-----------
ch_id | cu_id
1 | 1001
2 | 1002
3 | 1002
操作都還算方便,相較easyexcel來說EEC要更簡單一點国撵,如果不輸出worksheet名那么一行命令就可以完成輸出reader.sheets().flatMap(Sheet::rows).forEach(System.out::println);
4. EEC更多使用方式
由于EEC采用迭代模式因此可以使用JDK8的Stream全部功能陵吸,下面展示一些常用功能。
4.1 將內(nèi)容轉(zhuǎn)為集合
數(shù)據(jù)量小的時候可以將數(shù)據(jù)全部放入內(nèi)存像下面這樣
try (ExcelReader reader = ExcelReader.read(defaultTestPath.resolve("Large easyexcel.xlsx"))) {
List<LargeData> list = reader.sheets().flatMap(Sheet::dataRows)
.map(row -> row.to(LargeData.class)).collect(Collectors.toList());
// 保存到數(shù)據(jù)庫
save(list);
} catch (IOException e) {
e.printStackTrace();
}
當然我們誰也無法預料文件中有多少數(shù)據(jù)量介牙,直接轉(zhuǎn)為集合可能產(chǎn)生OOM壮虫,此時你可以通過sheet#getDimension
方法先獲取Worksheet的維度再按實際情況來選擇執(zhí)行方式,就像下面這樣:
public void test11() {
try (ExcelReader reader = ExcelReader.read(defaultTestPath.resolve("test8.xlsx"))) {
Sheet firstSheet = reader.sheet(0);
Dimension dimension = firstSheet.getDimension();
// lastRow - firstRow = 數(shù)據(jù)行的行數(shù),不包含header
if (dimension.lastRow - dimension.firstRow > 1000) {
// 如果數(shù)據(jù)量超過1千則選擇流式處理囚似,forEach里也可以收集一定量的實體再批量處理
firstSheet.dataRows().map(row -> row.too(Check.class)).forEach(check -> {
// TODO 業(yè)務(wù)處理
});
} else {
// 數(shù)據(jù)量小于1千則直接轉(zhuǎn)為集合處理
List<Check> checks = firstSheet.dataRows().map(row -> row.to(Check.class)).collect(Collectors.toList());
// TODO 業(yè)務(wù)處理
}
} catch (IOException e) {
e.printStackTrace();
}
}
4.2 取單列數(shù)據(jù)
EEC提供與JDBC類似的接口剩拢,用戶可以使用row.getX(columnNumber)
獲取指定位置的值,對于讀非規(guī)則表格或非表格時這是非常有效的饶唤。
示例:獲取"二年級學生.xlsx"中所有學生的姓名并去重
try (ExcelReader reader = ExcelReader.read(defaultTestPath.resolve("二年級學生.xlsx"))) {
List<String> names = reader.sheet(0) // 只取第一個worksheet頁
.dataRows()
.map(row -> row.getString("姓名")) // 只取姓名列
.distinct() // 去重
.collect(Collectors.toList());
// 業(yè)務(wù)處理
} catch (IOException e) {
e.printStackTrace();
}
4.3 過濾某些行
我相信很多時候都會遇到這樣的需求徐伐,我們僅需要處理滿足某些要求的數(shù)據(jù)而過濾掉檢查失敗的數(shù)據(jù),這時候filter
就派上用場了
比如我們需要打印帳單頁金額大于100的記錄
public void test9() {
try (ExcelReader reader = ExcelReader.read(defaultTestPath.resolve("test8.xlsx"))) {
reader.sheet("帳單表")
.dataRows()
.filter(row -> row.getDouble("total") > 100.0)
.forEach(System.out::println);
} catch (IOException e) {
e.printStackTrace();
}
}
// 輸出結(jié)果
1 | 100.8
3 | 983.0
4.4 其它一些亮眼功能
EEC還有一些比較亮眼的功能如高亮募狂,水印等一些實用的功能办素,下面代碼展示如何將低于60分的學生標紅且將分數(shù)顯示為不及格
public void testStyleConversion() throws IOException {
new Workbook("testStyleConversion") // 文件名
.setCreator("奈留·智庫") // 作者
.setCompany("Copyright (c) 2020") // 公司名
.setWaterMark(WaterMark.of("Secret")) // 水印
.setAutoSize(true) // 自動計算列寬
.addSheet(new ListSheet<>("期末成績", Student.randomTestData(20)
, new org.ttzero.excel.entity.Sheet.Column("學號", "id", int.class)
, new org.ttzero.excel.entity.Sheet.Column("姓名", "name", String.class)
, new org.ttzero.excel.entity.Sheet.Column("成績", "score", int.class)
// 低于60分顯示`不及格`
.setProcessor(n -> n < 60 ? "不及格" : n)
// 低于60分單元格標紅
.setStyleProcessor((o, style, sst) -> {
if ((int)o < 60) {
style = Styles.clearFill(style) | sst.addFill(new Fill(Color.red));
}
return style;
})
)
)
.writeTo(defaultTestPath);
}
最終生成文件如下
5. 后記
讀excel時不要試圖將數(shù)據(jù)轉(zhuǎn)為Map類型,因為每個Map都需要保存表頭和單元格值祸穷,這將極大的消耗內(nèi)存性穿。
簡單總結(jié): easyexcel和EEC兩個工具都極大的簡化了java操作excel,從原本Apache POI繁鎖的API和高內(nèi)存中解脫出來雷滚。其中easyexcel支持更低的JDK版本季二,而EEC使用了更靈活的設(shè)計模式,同時所有的底層代碼均是獨立實現(xiàn)揭措,意味著它的依懶非常小胯舷。但是EEC現(xiàn)階段鮮有人使用有很多BUG也就無從發(fā)現(xiàn),穩(wěn)定性還有待考驗绊含。