EasyExcel 轻松灵活读取Excel内容
写在前面
java 后端程序员应该会遇到读取 excel 信息到 db 等相关需求,脑海中可能突然间想起 apache poi 这个技术解决方案,但是当 excel 的数据量非常大的时候,你也许发现,poi 是将整个 excel 的内容全部读出来放入到内存中,所以内存消耗非常严重,如果同时进行包含大数据量的 excel 读操作,很容易造成内存溢出问题
但 easyexcel 的出现很好的解决了 poi 相关问题,原本一个 3m 的 excel 用 poi 需要100m左右内存, 而 easyexcel 可以将其降低到几 m,同时再大的 excel 都不会出现内存溢出的情况,因为是逐行读取 excel 的内容 (老规矩,这里不用过分关心下图,脑海中有个印象即可,看完下面的用例再回看这个图,就很简单了)
另外 easyexcel 在上层做了模型转换的封装,不需要 cell 等相关操作,让使用者更加简单和方便,且看
简单读
假设我们 excel 中有以下内容:
我们需要新建 user 实体,同时为其添加成员变量
@data public class user { /** * 姓名 */ @excelproperty(index = 0) private string name; /** * 年龄 */ @excelproperty(index = 1) private integer age; }
你也许关注到了 @excelproperty
注解,同时使用了 index 属性 (0 代表第一列,以此类推),该注解同时支持以「列名」name 的方式匹配,比如:
@excelproperty("姓名") private string name;
按照 github 文档的说明:
不建议 index 和 name 同时用,要么一个对象只用index,要么一个对象只用name去匹配
- 如果读取的 excel 模板信息列固定,这里建议以 index 的形式使用,因为如果用名字去匹配,名字重复,会导致只有一个字段读取到数据,所以 index 是更稳妥的方式
- 如果 excel 模板的列 index 经常有变化,那还是选择 name 方式比较好,不用经常性修改实体的注解 index 数值
所以大家可以根据自己的情况自行选择
编写测试用例
easyexcel 类中重载了很多个 read 方法,这里不一一列举说明,请大家自行查看;同时 sheet 方法也可以指定 sheetno,默认是第一个 sheet 的信息
上面代码的 new userexcellistener()
异常醒目,这也是 easyexcel 逐行读取 excel 内容的关键所在,自定义 userexcellistener
继承 analysiseventlistener
@slf4j public class userexcellistener extends analysiseventlistener<user> { /** * 批处理阈值 */ private static final int batch_count = 2; list<user> list = new arraylist<user>(batch_count); @override public void invoke(user user, analysiscontext analysiscontext) { log.info("解析到一条数据:{}", json.tojsonstring(user)); list.add(user); if (list.size() >= batch_count) { savedata(); list.clear(); } } @override public void doafterallanalysed(analysiscontext analysiscontext) { savedata(); log.info("所有数据解析完成!"); } private void savedata(){ log.info("{}条数据,开始存储数据库!", list.size()); log.info("存储数据库成功!"); } }
到这里请回看文章开头的 easyexcel 原理图,invoke 方法逐行读取数据,对应的就是订阅者 1;doafterallanalysed 方法对应的就是订阅者 2,这样你理解了吗?
打印结果:
从这里可以看出,虽然是逐行解析数据,但我们可以自定义阈值,完成数据的批处理操作,可见 easyexcel 操作的灵活性
自定义转换器
这是最基本的数据读写,我们的业务数据通常不可能这么简单,有时甚至需要将其转换为程序可读的数据
性别信息转换
比如 excel 中新增「性别」列,其性别为男/女,我们需要将 excel 中的性别信息转换成程序信息: 「1: 男;2:女」
首先在 user 实体中添加成员变量 gender:
@excelproperty(index = 2) private integer gender;
easyexcel 支持我们自定义 converter,将 excel 的内容转换为我们程序需要的信息,这里新建 genderconverter,用来转换性别信息
public class genderconverter implements converter<integer> { public static final string male = "男"; public static final string female = "女"; @override public class supportjavatypekey() { return integer.class; } @override public celldatatypeenum supportexceltypekey() { return celldatatypeenum.string; } @override public integer converttojavadata(celldata celldata, excelcontentproperty excelcontentproperty, globalconfiguration globalconfiguration) throws exception { string stringvalue = celldata.getstringvalue(); if (male.equals(stringvalue)){ return 1; }else { return 2; } } @override public celldata converttoexceldata(integer integer, excelcontentproperty excelcontentproperty, globalconfiguration globalconfiguration) throws exception { return null; } }
上面程序的 converter 接口的泛型是指要转换的 java 数据类型,与 supportjavatypekey 方法中的返回值类型一致
打开注解 @excelproperty
查看,该注解是支持自定义 converter 的,所以我们为 user 实体添加 gender 成员变量,并指定 converter
/** * 性别 1:男;2:女 */ @excelproperty(index = 2, converter = genderconverter.class) private integer gender;
来看运行结果:
数据按照我们预期做出了转换,从这里也可以看出,converter 可以一次定义到处是用的便利性
日期信息转换
日期信息也是我们常见的转换数据,比如 excel 中新增「出生年月」列,我们要解析成 yyyy-mm-dd 格式,我们需要将其进行格式化,easyexcel 通过 @datetimeformat
注解进行格式化
在 user 实体中添加成员变量 birth,同时应用 @datetimeformat
注解,按照要求做格式化
/** * 出生日期 */ @excelproperty(index = 3) @datetimeformat("yyyy-mm-dd hh:mm:ss") private string birth;
来看运行结果:
如果这里你指定 birth 的类型为 date,试试看,你得到的结果是什么?
到这里都是以测试的方式来编写程序代码,作为 java web 开发人员,尤其在目前主流 spring boot 的架构下,所以如何实现 web 方式读取 excel 的信息呢?
web 读
简单 web
很简单,只是将测试用例的关键代码移动到 controller 中即可,我们新建一个 usercontroller
,在其添加 upload
方法
@restcontroller @requestmapping("/users") @slf4j public class usercontroller { @postmapping("/upload") public string upload(multipartfile file) throws ioexception { easyexcel.read(file.getinputstream(), user.class, new userexcellistener()).sheet().doread(); return "success"; } }
其实在写测试用例的时候你也许已经发现,listener 是以 new 的形式作为参数传入到 easyexcel.read 方法中的,这是不符合 spring ioc 的规则的,我们通常读取 excel 数据之后都要针对读取的数据编写一些业务逻辑的,而业务逻辑通常又会写在 service 层中,我们如何在 listener 中调用到我们的 service 代码呢?
先不要向下看,你脑海中有哪些方案呢?
匿名内部类方式
匿名内部类是最简单的方式,我们需要先新建 service 层的信息:
新建 iuser 接口:
public interface iuser { public boolean savedata(list<user> users); }
新建 iuser 接口实现类 userserviceimpl:
@service @slf4j public class userserviceimpl implements iuser { @override public boolean savedata(list<user> users) { log.info("userservice {}条数据,开始存储数据库!", users.size()); log.info(json.tojsonstring(users)); log.info("userservice 存储数据库成功!"); return true; } }
接下来,在 controller 中注入 iuser:
@autowired private iuser iuser;
修改 upload 方法,以匿名内部类重写 listener 方法的形式来实现:
@postmapping("/uploadwithanonyinnerclass") public string uploadwithanonyinnerclass(multipartfile file) throws ioexception { easyexcel.read(file.getinputstream(), user.class, new analysiseventlistener<user>(){ /** * 批处理阈值 */ private static final int batch_count = 2; list<user> list = new arraylist<user>(); @override public void invoke(user user, analysiscontext analysiscontext) { log.info("解析到一条数据:{}", json.tojsonstring(user)); list.add(user); if (list.size() >= batch_count) { savedata(); list.clear(); } } @override public void doafterallanalysed(analysiscontext analysiscontext) { savedata(); log.info("所有数据解析完成!"); } private void savedata(){ iuser.savedata(list); } }).sheet().doread(); return "success"; }
查看结果:
这种实现方式,其实这只是将 listener 中的内容全部重写,并在 controller 中展现出来,当你看着这么臃肿的 controller 是不是非常难受?很显然这种方式不是我们的最佳编码实现
构造器传参
在之前分析 springboot 统一返回源码时,不知道你是否发现,spring 底层源码多数以构造器的形式传参,所以我们可以将为 listener 添加有参构造器,将 controller 中依赖注入的 iuser 以构造器的形式传入到 listener :
@slf4j public class userexcellistener extends analysiseventlistener<user> { private iuser iuser; public userexcellistener(iuser iuser){ this.iuser = iuser; } // 省略相应代码... private void savedata(){ iuser.savedata(list); //调用 userservice 中的 savedata 方法 }
更改 controller 方法:
@postmapping("/uploadwithconstructor") public string uploadwithconstructor(multipartfile file) throws ioexception { easyexcel.read(file.getinputstream(), user.class, new userexcellistener(iuser)).sheet().doread(); return "success"; }
运行结果: 同上
这样更改后,controller 代码看着很清晰,但如果后续业务还有别的 service 需要注入,我们难道要一直添加有参构造器吗?很明显,这种方式同样不是很灵活。
其实在使用匿名内部类的时候,你也许会想到,我们可以通过 java8 lambda 的方式来解决这个问题
lambda 传参
为了解决构造器传参的痛点,同时我们又希望 listener 更具有通用性,没必要为每个 excel 业务都新建一个 listener,因为 listener 都是逐行读取 excel 数据,只需要将我们的业务逻辑代码传入给 listener 即可,所以我们需用到 consumer<t>
,将其作为构造 listener 的参数。
新建一个工具类 exceldemoutils,用来构造 listener:
我们看到,getlistener 方法接收一个 consumer<list<t>>
的参数,这样下面代码被调用时,我们的业务逻辑也就会被相应的执行了:
consumer.accept(linkedlist);
继续改造 controller 方法:
运行结果: 同上
到这里,我们只需要将业务逻辑定制在 batchinsert
方法中:
- 满足 controller restful api 的简洁性
- listener 更加通用和灵活,它更多是扮演了抽象类的角色,具体的逻辑交给抽象方法的实现来完成
- 业务逻辑可扩展性也更好,逻辑更加清晰
总结
到这里,关于如何使用 easyexcel 读取 excel 信息的基本使用方式已经介绍完了,还有很多细节内容没有讲,大家可以自行查阅 easyexcel github 文档去发现更多内容。灵活使用 java 8 的函数式接口,更容易让你提高代码的复用性,同时看起来更简洁规范
除了读取 excel 的读取,还有 excel 的写入,如果需要将其写入到指定位置,配合 hutool 的工具类 filewriter 的使用是非常方便的,针对 easyexcel 的使用,如果大家有什么问题,也欢迎到博客下方探讨
完整代码请在公众号回复「demo」,点开链接,查看「easy-excel-demo」文件夹的内容即可,另外个人博客由于特殊原因暂时关闭首页,其他目录访问一切正常,更多文章可以从 https://dayarch.top/archives 入口查看
感谢
非常感谢 easyexcel 的作者
上一篇: c++ 内存二进制表示
下一篇: 幽默新鲜的雷言笑语
推荐阅读
-
Python读取txt内容写入xls格式excel中的方法
-
EasyExcel 轻松灵活读取Excel内容
-
纯JS实现的读取excel文件内容功能示例【支持所有浏览器】
-
XLSX从第几行数读取excel内容,去除换行符,两端空格,保持日期格式,加密软件提示,null赋值为空字符串
-
pandas按照excel表名(sheet)读取内容
-
使用VBA读取指定Excel文件指定Sheet指定单元格的内容
-
Java读取Excel数据内容,兼容excel2003和excel2007版本/xls后缀,xlsx后缀
-
java poi 导入excel时 读取单元格内容的方法 ,其中包含excel中有函数的读法
-
PHP读取excel文件内容、获取单元格数据
-
C#使用oledb读取excel表格内容到datatable的方法