当前位置: 首页 > news >正文

.net 逐行读取大文本文件_如何使用 Java 灵活读取 Excel 内容 ?

写在前面

Java 后端程序员应该会遇到读取 Excel 信息到 DB 等相关需求,脑海中可能突然间想起 Apache POI 这个技术解决方案,但是当 Excel 的数据量非常大的时候,你也许发现,POI 是将整个 Excel 的内容全部读出来放入到内存中,所以内存消耗非常严重,如果同时进行包含大数据量的 Excel 读操作,很容易造成内存溢出问题。

但 EasyExcel 的出现很好的解决了 POI 相关问题,原本一个 3M 的 Excel 用 POI 需要100M左右内存, 而 EasyExcel 可以将其降低到几 M,同时再大的 Excel 都不会出现内存溢出的情况,因为是逐行读取 Excel 的内容 (老规矩,这里不用过分关心下图,脑海中有个印象即可,看完下面的用例再回看这个图,就很简单了)

0de42c7964d8cf5e281d8e933cd5803a.png

另外 EasyExcel 在上层做了模型转换的封装,不需要 cell 等相关操作,让使用者更加简单和方便,且看

简单读

假设我们 excel 中有以下内容:

ec72b34c93a921d0bf8205346d957351.png

我们需要新建 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 数值所以大家可以根据自己的情况自行选择

编写测试用例

6e4a87d4729ca4858e886e8b380de28b.png

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,这样你理解了吗?打印结果:

b1b9d277328e305afb873c4f4a59848f.png

从这里可以看出,虽然是逐行解析数据,但我们可以自定义阈值,完成数据的批处理操作,可见 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

@ExcelProperty(index = 2, converter = GenderConverter.class)
private Integer gender;

来看运行结果:

e76b05c36a2030911d5acddecdc4a234.png

数据按照我们预期做出了转换,从这里也可以看出,Converter 可以一次定义到处是用的便利性

日期信息转换

日期信息也是我们常见的转换数据,比如 Excel 中新增「出生年月」列,我们要解析成yyyy-MM-dd格式,我们需要将其进行格式化,EasyExcel 通过@DateTimeFormat注解进行格式化

b415fb6d31b36b4b8bc4b3dc97b4dce4.png

在 User 实体中添加成员变量birth,同时应用@DateTimeFormat注解,按照要求做格式化

@ExcelProperty(index = 3)
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private String birth;

来看运行结果:

80cc747fd27f0ad9193c476e920545fa.png

如果这里你指定 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";
  }

查看结果:

eb6f4de2bcf9f47a76f99bd5d474dc20.png

这种实现方式,其实这只是将 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);
  }

更改 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:

ee2c21859693babd8fec7fab25adb5e2.png

我们看到,getListener 方法接收一个Consumer<List<T>>的参数,这样下面代码被调用时,我们的业务逻辑也就会被相应的执行了:

consumer.accept(linkedList); 

继续改造 Controller 方法:

339f088f203bbf413eebb6f4f2af76db.png

运行结果: 同上到这里,我们只需要将业务逻辑定制在batchInsert方法中:

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

总结

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

来源:日拱一兵

作者:日拱一兵

原文:如何使用 Java 灵活读取 Excel 内容 ?

相关文章:

  • bat脚本中如何多次键盘输入并判断_bat教程[282] @
  • java不同项目加token访问_接口测试彻底弄懂Session、Cookie、Token的区别及联系-hold住面试官开3万...
  • python operator 多属性排序_Python应用——自定义排序全套方案
  • 随机森林原始论文_初识随机森林
  • CNN Matlab例子RGB_CNN网络基础
  • js date 当前日志往后一个月_因为造轮子,我一个月就转正了 | 原力计划
  • apache tomcat下32还是64_Tomcat 下载、安装、配置图文教程
  • sqlserver安装目录_Windows 2016中安装SQLServer2016 Failover Cluster
  • springmvc @requestbody_【面试系列7】Spring MVC
  • python实现程序安装_python实现查找所有程序的安装信息
  • python中os模块_python中的os模块
  • 怎么python画好几朵玫瑰花_怎么用python画玫瑰花,求大神贴代码,感激不尽
  • 51单片机电路原理图_10个定时器精选电路方案带你学习时钟脉冲的工作方式
  • python读取数据的函数详解_python如何从文件读取数据及解析
  • wxpython的安装步骤_wxPython的安装图文教程(Windows)
  • 08.Android之View事件问题
  • ComponentOne 2017 V2版本正式发布
  • JavaScript DOM 10 - 滚动
  • java小心机(3)| 浅析finalize()
  • Nodejs和JavaWeb协助开发
  • Terraform入门 - 3. 变更基础设施
  • weex踩坑之旅第一弹 ~ 搭建具有入口文件的weex脚手架
  • 阿里云应用高可用服务公测发布
  • 道格拉斯-普克 抽稀算法 附javascript实现
  • 名企6年Java程序员的工作总结,写给在迷茫中的你!
  • 浅谈Golang中select的用法
  • 让你成为前端,后端或全栈开发程序员的进阶指南,一门学到老的技术
  • 使用SAX解析XML
  • ​io --- 处理流的核心工具​
  • (react踩过的坑)antd 如何同时获取一个select 的value和 label值
  • (ZT)出版业改革:该死的死,该生的生
  • (分享)自己整理的一些简单awk实用语句
  • (附源码)spring boot校园健康监测管理系统 毕业设计 151047
  • (三)Pytorch快速搭建卷积神经网络模型实现手写数字识别(代码+详细注解)
  • (原創) 如何刪除Windows Live Writer留在本機的文章? (Web) (Windows Live Writer)
  • *Django中的Ajax 纯js的书写样式1
  • ./configure,make,make install的作用
  • ./和../以及/和~之间的区别
  • .NET CORE 第一节 创建基本的 asp.net core
  • .net core IResultFilter 的 OnResultExecuted和OnResultExecuting的区别
  • .net core webapi 大文件上传到wwwroot文件夹
  • .net mvc actionresult 返回字符串_.NET架构师知识普及
  • .NET/C# 避免调试器不小心提前计算本应延迟计算的值
  • .NET/C# 中你可以在代码中写多个 Main 函数,然后按需要随时切换
  • .netcore 6.0/7.0项目迁移至.netcore 8.0 注意事项
  • .NET多线程执行函数
  • .pings勒索病毒的威胁:如何应对.pings勒索病毒的突袭?
  • /run/containerd/containerd.sock connect: connection refused
  • [ vulhub漏洞复现篇 ] AppWeb认证绕过漏洞(CVE-2018-8715)
  • [BZOJ 1040] 骑士
  • [CC2642R1][VSCODE+Embedded IDE+IAR Build+Cortex-Debug] TI CC2642R1基于VsCode的开发环境
  • [ffmpeg] aac 音频编码
  • [Foreman]解决Unable to find internal system admin account
  • [GDOUCTF 2023]<ez_ze> SSTI 过滤数字 大括号{等
  • [Grafana]ES数据源Alert告警发送