写在前面

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去匹配

  1. 如果读取的 Excel 模板信息列固定,这里建议以 index 的形式使用,因为如果用名字去匹配,名字重复,会导致只有一个字段读取到数据,所以 index 是更稳妥的方式
  2. 如果 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 方法中:

  1. 满足 Controller RESTful API 的简洁性
  2. listener 更加通用和灵活,它更多是扮演了抽象类的角色,具体的逻辑交给抽象方法的实现来完成
  3. 业务逻辑可扩展性也更好,逻辑更加清晰

总结

到这里,关于如何使用 EasyExcel 读取 Excel 信息的基本使用方式已经介绍完了,还有很多细节内容没有讲,大家可以自行查阅 EasyExcel Github 文档去发现更多内容。灵活使用 Java 8 的函数式接口,更容易让你提高代码的复用性,同时看起来更简洁规范

除了读取 Excel 的读取,还有 Excel 的写入,如果需要将其写入到指定位置,配合 HuTool 的工具类 FileWriter 的使用是非常方便的,针对 EasyExcel 的使用,如果大家有什么问题,也欢迎到博客下方探讨

完整代码请在公众号回复「demo」,点开链接,查看「easy-excel-demo」文件夹的内容即可,另外个人博客由于特殊原因暂时关闭首页,其他目录访问一切正常,更多文章可以从 https://dayarch.top/archives 入口查看

感谢

非常感谢 EasyExcel 的作者

EasyExcel 轻松灵活读取Excel内容的更多相关文章

  1. POI读取Excel内容格式化

    在用POI读取Excel内容时,经常会遇到数据格式化的问题. 比如:数字12365会变为12365.0;字符串数字123也会变为123.0,甚至会被变为科学计数法.另外日期格式化也是一个头疼的问题.其 ...

  2. C# 读取Excel内容

    一.方法 1.OleD方法实现该功能. 2.本次随笔内容只包含读取Excel内容,并另存为. 二.代码 (1)找到文档代码 OpenFileDialog openFile = new OpenFile ...

  3. Unity用Excel.dll简单读取Excel内容

    Unity用Excel.dll简单读取Excel内容 需要Excel.dll 需要如下三个命名空间 using System.IO; using Excel; using System.Data; 1 ...

  4. java poi 读取excel内容

    import org.apache.poi.hssf.usermodel.HSSFWorkbook; import org.apache.poi.ss.usermodel.Row; import or ...

  5. Java读取Excel内容

    借助于apathe的poi.jar,由于上传文件不支持.jar所以请下载后将文件改为.jar,在应用程序中添加poi.jar包,并将需要读取的excel文件放入根目录即可 本例使用java来读取exc ...

  6. poi读取excel内容工具类

    该工具类可以读取excel2007,excel2003等格式的文件,xls.xlsx文件格式 package com.visolink; import org.apache.poi.hssf.user ...

  7. .NET小笔记-NPOI读取excel内容到DataTable

    下载比较新的NPOI组件支持excel2007以上的,把.dll添加引用 引入命名空间 using NPOI.HSSF.UserModel;using NPOI.SS.UserModel;using ...

  8. nodejs读取excel内容批量替换并生成新的html和新excel对照文件

    因为广告投放需要做一批对外投放下载页面,由于没有专门负责填充页面的编辑同学做,只能前端来做了, 拿到excel看了一下,需要生成200多个文件,一下子懵逼了. 这要是来回复制粘贴太low了 正好最新用 ...

  9. Java 读取Excel内容并保存进数据库

    读取Excel中内容,并保存进数据库 步骤 建立数据库连接 读取文件内容 (fileInputStream 放进POI的对应Excel读取接口,实现Excel文件读取) 获取文件各种内容(总列数,总行 ...

随机推荐

  1. [币严区块链]简单易懂的以太坊(ETH)智能合约开发入门教程

    以太坊(Ethereum)是一提供个智能合约(smart contract)功能的公共区块链(BlockChain)平台. 本文介绍了一个简单的以太坊智能合约的开发过程. 开发环境 在以太坊上开发应用 ...

  2. ubuntu修改中文文件夹名字为英文

    为了使用起来方便,装了ubuntu中文版,自然在home文件里用户目录的“桌面”.“图片”.“视频”.“音乐”……都是中文的. 很多时候都喜欢在桌面上放一些要操作的文件,Linux里命令行操作又多,难 ...

  3. 装系统------- 了解常用的启动方式以及如何进入bios

    1.从硬盘启动:这种方式提供了最简单的维护解决方案,其基本原理就是增加一个系统的开机启动项,每次开机的时候您都可以选择是进入本地系统还是进入PE. 安装程序并不将PE的启动项作为默认启动项,而是提供一 ...

  4. Java第二次作业第三题

    四叶玫瑰线的图形设计:当用鼠标拖拽改变窗口大小时,四叶玫瑰线会重新绘制 package naizi; import java.awt.*; import java.awt.event.*; impor ...

  5. 阿里云服务器CentOS7.3上通过Docker安装MySQL

    一.前言 我的服务器环境: CentOS7.3 Docker Portainer -> Docker可视化界面工具 二.拉取mysql镜像 这里我安装的是mysql5.7版本 docker pu ...

  6. .NET Conf 2019 大会上发布.NET Core 3.0

    北京时间今天凌晨如期在.NET Conf 上发布.NET Core 3.0,Keynotes 由Scott Hunter 主演,主要围绕.NET Core 3.0的新特性和社区展开. 多功能性是.Ne ...

  7. java httpclient跳过https证书验证

    httpclien调用skipHttpsUtil得wrapClient方法跳过https证书验证 SkipHttpsUtil  skipHttpsUtil=new SkipHttpsUtil();   ...

  8. centos赋予访问权限

    chmod u+x *.sh (代表授权现在所处的文件目录下的所有以:.sh的文件 启动tomcat:/usr/local/tomcat9/bin/shutdown.sh 关闭tomcat:/usr/ ...

  9. RabbitMQ的六种工作模式总结

    最近学习RabbitMQ的使用方式,记录下来,方便以后使用,也方便和大家共享,相互交流. RabbitMQ的六种工作模式: 1.Work queues2.Publish/subscribe3.Rout ...

  10. vscode中自动补全<?php?>

    方法引用自百度知道的一个回答: 但是他这个我用着需要优化一下,我的代码是: "PHP":{ "prefix": "php", "b ...