Excel操作工具(Easyexcel vs EEC)比照(一)
点击这里查看原文
本系列主要详情Easyexcel和EEC的功能并从便利性、性能、内存等多方面全面的进行评测。
1. 开始
关于Easyexcel
easyexcel是alibaba开发的快速、简单、且避免OOM的java解决Excel工具,于2018.2在github上开源。它是在Apache POI基础上包装而来,主要处理Apache POI高内存且API臃肿的诟病,easyexcel提供了比原生的POI简结很多的接口,读写Excel文件均可以一行代码完成,目前(2020.4)github上有14.2k个Star和3.7K个Fork。
引用作者总结核心原理:
- 文件解压、读取通过文件形式
- 避免将一律数据一次加载到内存(采用sax模式一行一行解析并使用观察者的模式通知解决)
- 抛弃不重要的数据(忽略样式,字体,宽度等数据)
点击这里查看作者原文
关于EEC
EEC是国内一个个人开发者开发并于2017.10月在github开源,EEC的底层并没有使用Apache POI包,所有的底层读写代码均由作者实现,事实上EEC仅依懒dom4j和slf4j,前者用于小文件xml读取,后者统一日志接口。
核心原理:
- 不缓存数据或者一些缓存
- 使用分片来解决较大的数据
- 单元格样式仅使用一个int值来保存,极大缩小内存使用
- 使用迭代模式读取行内容,不会将整个文件读入到内存
简单总结两个工具的不同:
- 底层不同,easyexcel底层使用Apache POI,EEC使用IO/NIO
- easyexcel最低支持JDK7,EEC最低支持JDK8
- easyexcel简化了接口使得像设置样式这种基本功能非常困难,EEC默认带有便于阅读的样式也提供方法设置其它样式
- easyexcel读取文件时忽略样式和字体也没有办法直接获取单元格的公式。
- easyexcel对常用类型缺少支持(char, Timestamp, Time, LocalDate, LocalDateTime, LocalTime),假如实体类中有这些类型就必需为这些类型编写自己设置Converter
相比之下EEC更接近于Apache POI,而easyexcel更关注单元格的值而忽略其它不太关心的数据。
2. 写文件
2.1 一些数据
对于一些数据可以直接将内容放到数组/集合中一次写入,两个工具都能做到一行代码完成数据写入。下面展现两者的实现方式,代码中出现的defaultTestPath是文件路径事前已创立好。
easyexcel可以将文件直接写入OutputStream或者磁盘
public void test5(List<Item> data) { EasyExcel.write(defaultTestPath.resolve("test5.xlsx").toString(), LargeData.class).sheet().doWrite(data);}
test5.xlsx
EEC同样可以写入OutputStream或者磁盘,使用writeTo方法指定输出位置
public void test6(List<Item> data) throws IOException { new Workbook("test6").addSheet(new ListSheet<>(data)).writeTo(defaultTestPath);}
test6.xlsx
2.2 写多个worksheet页
两个工具都提供便利的方法实现多worksheet页写入,基本可以使用一行代码搞定。
easyexcel通过创立多个WriteSheet来实现
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("客户用户关系表").build()) .finish();}
test7.xlsx
EEC通过addSheet方法增加多个worksheet,看上去更直观更容易了解。
public void test8() throws IOException { new Workbook("test8") .addSheet(new ListSheet<>("帐单表", checks())) .addSheet(new ListSheet<>("用户表", customers())) .addSheet(new ListSheet<>("客户用户关系表", c2CS())) .writeTo(defaultTestPath);}
test8.xlsx
2.3 大数据量
数据量较大时我们无法将数据一律装载到内存,此时需要分批写文件,好在easyexcel和EEC均支持分片解决做到边读数据边写文件。
easyexcel写大文件需要指定一个模板文件,而后调用fill方法循环写数据,需要注意假如数据量超出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方法并返回批量数据
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()方法模拟取数据过程,返回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推送数据,EEC采用pull方式由工具决定何时拉取下一块数据,返回空数组或者null时表明没有更多数据,所以这里要注意控制分页参数防止出现死循环。
对于数据量巨大且使用关系型数据库的场景,EEC提供另一种方案,客户可以使用StatementSheet和ResultSetSheet两种方式,它们的工作方式是将SQL和参数交给EEC,EEC内部去查询并使用游标做到取一个值写一个值,省掉了将表数据转为Java实体的过程。
3. 读文件
easyexcel在读文件时使用ReadListener来监听每行数据,这样可以做到边解析文件边做业务逻辑(插库或者其它),不用把文件解析完成后再做业务逻辑,以下是解析图示:
easyexcel解析图示
EEC采用迭代模式,同样做到边解析文件边做业务逻辑,处理POI的高内存问题。
eec解析图示
从两者图示大致可以看出两者的设计与写文件时正好相反。easyexcel通过监听主动把行数据推给客户,EEC这边需要客户主动拉数据,只有当客户真正需要某行数据时才去解析它们来实现推迟读取。
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) { // 业务解决 } @Override public void doAfterAllAnalysed(AnalysisContext context) { } }).headRowNumber(1).sheet().doRead();}你需要实现一个ReaderListener来解决行数据。
3.2 EEC读文件
try (ExcelReader reader = ExcelReader.read(defaultTestPath.resolve("Large easyexcel.xlsx"))) { reader.sheet("帐单表") // 解析指定worksheet .flatMap(Sheet::dataRows) // 只取数据行,跳过表头 .map(row -> row.to(LargeData.class)) // 转为实体对象 .forEach(o -> { // 业务解决 });} catch (IOException e) { e.printStackTrace();}EEC引入java8的stream+lambda功能,你可以像操作集合类一样来操作Excel,而不用担心OOM发生。
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); });}// 输出内容----------帐单表-----------{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=李四}----------客户用户关系表-----------{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(); }}// 输出内容----------帐单表-----------id | total1 | 100.82 | 34.23 | 983----------用户表-----------id | name1001 | 张三1002 | 李四----------客户用户关系表-----------ch_id | cu_id1 | 10012 | 10023 | 1002操作都还算方便,相较easyexcel来说EEC要更简单一点,假如不输出worksheet名那么一行命令即可以完成输出reader.sheets().flatMap(Sheet::rows).forEach(System.out::println);
4. EEC更多使用方式
因为EEC采用迭代模式因而可以使用JDK8的Stream一律功能,下面展现少量常用功能。
4.1 将内容转为集合
数据量小的时候可以将数据一律放入内存像下面这样
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()); // 保存到数据库 save(list);} catch (IOException e) { e.printStackTrace();}当然我们谁也无法意料文件中有多少数据量,直接转为集合可能产生OOM,此时你可以通过sheet#getDimension方法先获取Worksheet的维度再按实际情况来选择执行方式,就像下面这样:
public void test11() { try (ExcelReader reader = ExcelReader.read(defaultTestPath.resolve("test8.xlsx"))) { Sheet firstSheet = reader.sheet(0); Dimension dimension = firstSheet.getDimension(); // lastRow - firstRow = 数据行的行数,不包含header if (dimension.lastRow - dimension.firstRow > 1000) { // 假如数据量超过1千则选择流式解决,forEach里也可以收集肯定量的实体再批量解决 firstSheet.dataRows().map(row -> row.too(Check.class)).forEach(check -> { // TODO 业务解决 }); } else { // 数据量小于1千则直接转为集合解决 List<Check> checks = firstSheet.dataRows().map(row -> row.to(Check.class)).collect(Collectors.toList()); // TODO 业务解决 } } catch (IOException e) { e.printStackTrace(); }}4.2 取单列数据
EEC提供与JDBC相似的接口,客户可以使用row.getX(columnNumber)获取指定位置的值,对于读非规则表格或者非表格时这是非常有效的。
示例:获取”二年级学生.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()); // 业务解决} catch (IOException e) { e.printStackTrace();}4.3 过滤某些行
我相信很多时候都会遇到这样的需求,我们仅需要解决满足某些要求的数据而过滤掉检查失败的数据,这时候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(); }}// 输出结果1 | 100.83 | 983.04.4 其它少量亮眼功能
EEC还有少量比较亮眼的功能如高亮,水印等少量实用的功能,下面代码展现如何将低于60分的学生标红且将分数显示为不及格
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时不要试图将数据转为Map类型,由于每个Map都需要保存表头和单元格值,这将极大的消耗内存。
简单总结: easyexcel和EEC两个工具都极大的简化了java操作excel,从本来Apache POI繁锁的API和高内存中解脱出来。其中easyexcel支持更低的JDK版本,而EEC使用了更灵活的设计模式,同时所有的底层代码均是独立实现,意味着它的依懒非常小。但是EEC现阶段鲜有人使用有很多BUG也就无从发现,稳固性还有待考验。
下一篇将比照两者在各数据量下的读写性能
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » Excel操作工具(Easyexcel vs EEC)比照(一)