点击蓝色“程序猿DD”关注我

回复“资源”获取独家整理的学习资料!

作者 | 优雅先生

来源 | my.oschina.net/feichexia/blog/196575

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 的作者

读取Excel还用POI?试试这款开源工具相关推荐

  1. 你只需画草稿,剩下都交给AI!哈佛『机器学习』最新课程;Evernote收费又难用?试试这款开源工具;提示工程资源整合笔记;前沿论文 | ShowMeAI资讯日报

  2. 还没使用过Web Worker,推荐一款开源工具Workerize,快速上手

    还没使用过Web Worker,推荐一款开源工具Workerize,快速上手 开源项目:https://github.com/developit/workerize 将模块移动到 Web 辅助角色中, ...

  3. 还没使用过Web Worker? 推荐一款开源工具Workerize-Loader,让你在webpack项目中轻松使用Web Worker

    还没使用过Web Worker? 推荐一款开源工具Workerize-Loader,让你在webpack项目中轻松使用Web Worker Workerize-Loader 将模块及其依赖项移动到 W ...

  4. OhMyZsh是一款开源工具,可以用于管理Zsh(Linux命令解释器的一种)的配置

    最近在研究终端工具的时候,发现人家的终端可以输出各种彩色文字,还有各种提示,自己就算用了炫酷的Tabby也无法实现.后来发现需要在Linux上安装OhMyZsh才行,今天给大家介绍下这款功能强大,插件 ...

  5. 三款开源工具让你的演示脱颖而出

    本文转载至:http://blog.callmewhy.com/2014/07/02/three-open-source-tools-to-make-your-presentations-pop/ 不 ...

  6. java 读取Excel数据(POI)(一个sheet或者多个sheet)

    1.添加依赖 <dependency><groupId>org.apache.poi</groupId><artifactId>poi-ooxml< ...

  7. Typora 收费?试试这款开源 Markdown 神器!好用还美观

    点击关注公众号,回复"1024"获取2TB学习资源! Markdown是一种轻量级标记语言,创始人为约翰·格鲁伯(英语:John Gruber).它允许人们使用易读易写的纯文本格式 ...

  8. 试试54款开源服务器软件

    据斯坦福大学的咨询学教授Jonathon Koomey近期作所的一项调查显示,全球已安装的服务器总数约为3160万台,包括设在美国的大约1150万台.如果企业机构针对所有那些系统只能使用专有软件,因而 ...

  9. 专有软件不是唯一!试试54款开源服务器软件[转]

    据斯坦福大学的咨询学教授Jonathon Koomey近期作所的一项调查显示,全球已安装的服务器总数约为3160万台,包括设在美国的大约1150万台.如果企业机构针对所有那些系统只能使用专有软件,因而 ...

最新文章

  1. Android自定义ListView的Item无法响应OnItemClick的解决办法
  2. python50种算法_收藏 | 一文洞悉Python必备50种算法(附解析)
  3. 码农翻身讲计算机基础:并发,同步与信号量
  4. 【iOS atomic、nonatomic、assign、copy、retain、weak、strong】的定义和区别详解
  5. Vue nextTick执行时机分析
  6. 中小型研发团队架构落地实践18篇,含案例、代码
  7. Android开发之自定义UI组件和属性
  8. Cisco Packet Tracer 6.2 安装教程 | 计算机网络
  9. linux常用命令清单
  10. 锁存器芯片74HC573芯片的用法,及其在实际电路中的应用
  11. 《激荡三十年》十九、脚下的路——对中国经济未来的猜想
  12. SLAM学习——BA(Bundle Adjustment)与图优化
  13. php 中%3cspan%3e,vue实战(4)——网站统计之——友盟百度统计
  14. vc 热键、组合键的用法 MFC c++ hotkey WM_HOTKEY
  15. Python-pptx Table
  16. GitHub星数1.3W!五分钟带你搞定Bash脚本使用技巧
  17. 鸿蒙系统电脑适配双面打印机,win10系统实现打印机双面打印的操作方法
  18. 做短视频怎么赚钱,盈利模式包括哪些模式,如何做短视频自媒体赚钱
  19. 程序员的十层楼(http://softwareblogs-zho.intel.com/2009/02/04/1071/)
  20. 2020一战中科大计算机初复试经验贴

热门文章

  1. linux c getrlimit sysconf 系统限定 实例
  2. python 异或加密字符串
  3. docker logs 查看docker容器日志
  4. linux shell 代码太长换行 续行
  5. 软件版本中 release stable alpha beta pre snapshot 区别
  6. linux c 指针 内存 泄漏几种情况
  7. linux 本地socket 简介
  8. Web容器自动对HTTP请求中参数进行URLDecode处理
  9. Java Servlet工作原理问答
  10. 如何在Linux使用Eclipse + CDT开发C/C++程序? (OS) (Linux) (C/C++) (gcc) (g++)