EasyExcel 轻松灵活读取Excel内容
ztj100 2024-11-05 13:28 38 浏览 0 评论
写在前面
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 ,将其作为构造 listener 的参数。
新建一个工具类 ExcelDemoUtils,用来构造 listener:
我们看到,getListener 方法接收一个 Consumer > 的参数,这样下面代码被调用时,我们的业务逻辑也就会被相应的执行了:
consumer.accept(linkedList);
继续改造 Controller 方法:
运行结果: 同上
到这里,我们只需要将业务逻辑定制在 batchInsert 方法中:
- 满足 Controller RESTful API 的简洁性
- listener 更加通用和灵活,它更多是扮演了抽象类的角色,具体的逻辑交给抽象方法的实现来完成
- 业务逻辑可扩展性也更好,逻辑更加清晰
总结
到这里,关于如何使用 EasyExcel 读取 Excel 信息的基本使用方式已经介绍完了,还有很多细节内容没有讲,大家可以自行查阅 https://github.com/alibaba/easyexcel 文档去发现更多内容。灵活使用 Java 8 的函数式接口,更容易让你提高代码的复用性,同时看起来更简洁规范
除了读取 Excel 的读取,还有 Excel 的写入,如果需要将其写入到指定位置,配合 HuTool 的工具类 FileWriter 的使用是非常方便的,针对 EasyExcel 的使用,如果大家有什么问题,也欢迎到博客下方探讨
更好阅读体验:https://dayarch.top/p/61d8a472.html 另外个人博客由于特殊原因暂时关闭首页,其他目录访问一切正常,更多文章可以从 https://dayarch.top/archives 入口查看
感谢
非常感谢 EasyExcel 的作者 ,让 Excel 的读写更加方便
灵魂追问
- 除了 Consumer,如果需要返回值的业务逻辑,需要用到哪个函数式接口呢?
- 当出现复杂表头的时候要如何处理呢?
- 将 DB 数据写入到 Excel 并下载,如何实现呢?
- 从 EasyExcel 的设计上,你学到了什么,欢迎博客下方留言讨论
推荐阅读
- https://dayarch.top/p/d7e125a1.html
- https://dayarch.top/p/86243a5b.html
- https://dayarch.top/p/6d12b8cf.html
- https://dayarch.top/p/815d7647.html
- https://dayarch.top/p/32b8e26a.html
- https://dayarch.top/p/fb3c7eeb.html
--------
趣味原创解析Java技术栈问题,将复杂问题简单化,将抽象问题图形化落地
如果对我的专题内容感兴趣,或抢先看更多内容,欢迎访问我的博客 https://dayarch.top
相关推荐
- 作为后端开发,你知道MyBatis有哪些隐藏的 “宝藏” 扩展点吗?
-
在互联网大厂后端开发领域,MyBatis作为一款主流的持久层框架,凭借其灵活的配置与强大的数据处理能力,广泛应用于各类项目之中。然而,随着业务场景日趋复杂、系统规模不断扩张,开发过程中常面临SQL...
- 基于Spring+SpringMVC+Mybatis分布式敏捷开发系统架构(附源码)
-
前言zheng项目不仅仅是一个开发架构,而是努力打造一套从前端模板-基础框架-分布式架构-开源项目-持续集成-自动化部署-系统监测-无缝升级的全方位J2EE企业级开发解...
- 基于Java实现,支持在线发布API接口读取数据库,有哪些工具?
-
基于java实现,不需要编辑就能发布api接口的,有哪些工具、平台?还能一键发布、快速授权和开放提供给第三方请求调用接口的解决方案。架构方案设计:以下是一些基于Java实现的无需编辑或只需少量编辑...
- Mybatis Plus框架学习指南-第三节内容
-
自动填充字段基本概念MyBatis-Plus提供了一个便捷的自动填充功能,用于在插入或更新数据时自动填充某些字段,如创建时间、更新时间等。原理...
- 被你误删了的代码,在 IntelliJ IDEA中怎么被恢复
-
在IntelliJIDEA中一不小心将你本地代码给覆盖了,这个时候,你ctrl+z无效的时候,是不是有点小激动?我今天在用插件mybatisgenerator自动生成mapper的时候,...
- 修改 mybatis-generator 中数据库类型和 Java 类型的映射关系
-
使用mybatis-generator发现数据库类型是tinyint(4),生成model时字段类型是Byte,使用的时候有点不便数据库的类型和Model中Java类型的关系...
- 又被问到了, java 面试题:反射的实现原理及用途?
-
一、反射的实现原理反射(Reflection)是Java在运行时动态获取类的元数据(如方法、字段、构造器等)并操作类对象的能力。其核心依赖于...
- Spring Boot 中JPA和MyBatis技术那个更好?
-
你在进行SpringBoot项目开发时,是不是也经常在选择JPA和MyBatis这两个持久化技术上犯难?面对众多前辈的经验之谈,却始终拿不准哪种技术才最适合自己的项目?别担心,今天咱们就...
- Spring Boot (七)MyBatis代码自动生成和辅助插件
-
一、简介1.1MyBatisGenerator介绍MyBatisGenerator是MyBatis官方出品的一款,用来自动生成MyBatis的mapper、dao、entity的框架,让...
- 解决MyBatis Generator自动生成.java.1文件
-
MyBatis框架操作数据库,一张表对应着一个实体类、一个Mapper接口文件、一个Mapper映射文件。一个工程项目通常最少也要几十张表,那工作量可想而知非常巨大的,MyBatis框架替我们想好了解...
- Linux yq 命令使用详解
-
简介yq是一个轻量级、可移植的命令行...
- Python学不会来打我(62) json数据操作汇总
-
很多小伙伴学了很久的python一直还是没有把数据类型之间的转换搞明白,上一篇文章我们详细分享了python的列表、元组、字典、集合之间的相互转换,这一篇文章我们来分享json数据相关的操作,虽然严格...
- 之前3W买的Python全系列教程完整版(懂中文就能学会)
-
今天给大家带来了干货,Python入门教程完整版,完整版啊!完整版!言归正传,小编该给大家介绍一下这套教程了,希望每个小伙伴都沉迷学习,无法自拔...
- x-cmd pkg | grex - 正则表达式生成利器,解决手动编写的烦恼
-
简介grex是一个旨在简化创作正则表达式的复杂且繁琐任务的库和命令行程序。这个项目最初是DevonGovett编写的JavaScript工具regexgen的Rust移植。但re...
你 发表评论:
欢迎- 一周热门
- 最近发表
-
- 作为后端开发,你知道MyBatis有哪些隐藏的 “宝藏” 扩展点吗?
- 基于Spring+SpringMVC+Mybatis分布式敏捷开发系统架构(附源码)
- 基于Java实现,支持在线发布API接口读取数据库,有哪些工具?
- Mybatis Plus框架学习指南-第三节内容
- 被你误删了的代码,在 IntelliJ IDEA中怎么被恢复
- 修改 mybatis-generator 中数据库类型和 Java 类型的映射关系
- 又被问到了, java 面试题:反射的实现原理及用途?
- Spring Boot 中JPA和MyBatis技术那个更好?
- Spring Boot (七)MyBatis代码自动生成和辅助插件
- 解决MyBatis Generator自动生成.java.1文件
- 标签列表
-
- idea eval reset (50)
- vue dispatch (70)
- update canceled (42)
- order by asc (53)
- spring gateway (67)
- 简单代码编程 贪吃蛇 (40)
- transforms.resize (33)
- redisson trylock (35)
- 卸载node (35)
- np.reshape (33)
- torch.arange (34)
- npm 源 (35)
- vue3 deep (35)
- win10 ssh (35)
- vue foreach (34)
- idea设置编码为utf8 (35)
- vue 数组添加元素 (34)
- std find (34)
- tablefield注解用途 (35)
- python str转json (34)
- java websocket客户端 (34)
- tensor.view (34)
- java jackson (34)
- vmware17pro最新密钥 (34)
- mysql单表最大数据量 (35)