写在前面

Java后端程序员将会遇到诸如将Excel信息读取到DB等要求。突然会想到Apache POI这个技术解决方案,但是当Excel的数据量非常大的时候,就会发现POI把整个Excel的内容全部读取并放入内存中,所以内存消耗非常严重。如果同时进行包含大量数据的Excel。(威廉莎士比亚、Northern Excel、Northern Excel、Northern Excel)

但是EasyExcel的出现很好地解决了与POI相关的问题。原来3M的Excel需要100M左右的内存作为POI,EasyExcel可以将其减少到几米,更大的Excel不会发生内存溢出。因为这是逐行阅读Excel的内容。(现有规则不必过分注意下图。

此外,EasyExcel还在高级别创建了模型转换包。不需要cell等相关任务,用户可以轻松方便地查看。

简单阅读

假设Excel包含以下内容:

必须创建新的用户实体并添加成员变量

@Data

公共类用户{

/* *

*姓名

*/

@ExcelProperty(index=0)

私有字符串name

/* *

*年龄

*/

@ExcelProperty(index=1)

Private Integer age

}

您可以将@ExcelProperty标题与Index属性(第一行为0等)一起使用,以支持“列名”名称(例如:)的匹配。

@ExcelProperty(“名称”)

私有字符串name

根据Github文件中的准则:

不建议同时使用Index和name。建议一个对象只使用index,或者一个对象只匹配name

如果读取的Excel模板信息列是固定的,则最好使用index,因为匹配名称后,如果名称重复,则只有一个字段可以读取数据。因此,如果Excel模板中的列index经常更改,建议您选择name,而不必经常修改实体的注释index值。

创建测试用例

EasyExcel类中重载了许多read方法。这里不一一列举说明。请亲自确认。sheet方法也可以指定sheetNo,默认值是第一个sheet的信息

上述代码中的new UserExcelListener()异常引人注目。这是EasyExcel逐行阅读Excel内容的核心。自定义UserExcelListener继承AnalysisEventListener

@Slf4j

public class userexcellistener extends analysiseventlisteneruser {

/* *

*批处理阈值

*/

private static final int batch _ count=2;

listuser list=new ArrayList user(batch _ count);

@Override

Public void invoke (user user,analysis context analysis context){

Log.info('用一个数据解析)

:{}", JSON.toJSONString(user)); li(user); if () >= BATCH_COUNT) { saveData(); li(); } } @Override public void doAfterAllAnalysed(AnalysisContext analysisContext) { saveData(); log.info("所有数据解析完成!"); } private void saveData(){ log.info("{}条数据,开始存储数据库!", li()); 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 In; } @Override public CellDataTypeEnum supportExcelTypeKey() { return CellDa; } @Override public Integer convertToJavaData(CellData cellData, ExcelContentProperty excelContentProperty, GlobalConfiguration globalConfiguration) throws Exception { String stringValue = cellDa(); if (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 = GenderConver) 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 { Ea(), U, new UserExcelListener()).sheet().doRead(); return "success"; } }

其实在写测试用例的时候你也许已经发现,listener 是以 new 的形式作为参数传入到 Ea 方法中的,这是不符合 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 {}条数据,开始存储数据库!", u()); log.info(users)); log.info("UserService 存储数据库成功!"); return true; } }

接下来,在 Controller 中注入 IUser:

@Autowired private IUser iUser;

修改 upload 方法,以匿名内部类重写 listener 方法的形式来实现:

@PostMapping("/uploadWithAnonyInnerClass") public String uploadWithAnonyInnerClass(MultipartFile file) throws IOException { Ea(), U, 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)); li(user); if () >= BATCH_COUNT) { saveData(); li(); } } @Override public void doAfterAllAnalysed(AnalysisContext analysisContext) { saveData(); log.info("所有数据解析完成!"); } private void saveData(){ iU(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){ = iUser; } // 省略相应代码... private void saveData(){ iU(list); //调用 userService 中的 saveData 方法 }

更改 Controller 方法:

@PostMapping("/uploadWithConstructor") public String uploadWithConstructor(MultipartFile file) throws IOException { Ea(), U, new UserExcelListener(iUser)).sheet().doRead(); return "success"; }

运行结果: 同上

这样更改后,controller 代码看着很清晰,但如果后续业务还有别的 Service 需要注入,我们难道要一直添加有参构造器吗?很明显,这种方式同样不是很灵活。

其实在使用匿名内部类的时候,你也许会想到,我们可以通过 Java8 lambda 的方式来解决这个问题

Lambda 传参

为了解决构造器传参的痛点,同时我们又希望 listener 更具有通用性,没必要为每个 Excel 业务都新建一个 listener,因为 listener 都是逐行读取 Excel 数据,只需要将我们的业务逻辑代码传入给 listener 即可,所以我们需用到 Consumer ,将其作为构造 listener 的参数。

新建一个工具类 ExcelDemoUtils,用来构造 listener:

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

con(linkedList);

继续改造 Controller 方法:

运行结果: 同上

到这里,我们只需要将业务逻辑定制在 batchInsert 方法中:

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

总结

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

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

更好阅读体验: 另外个人博客由于特殊原因暂时关闭首页,其他目录访问一切正常,更多文章可以从 入口查看

感谢

非常感谢 EasyExcel 的作者 ,让 Excel 的读写更加方便

灵魂追问

  1. 除了 Consumer,如果需要返回值的业务逻辑,需要用到哪个函数式接口呢?
  2. 当出现复杂表头的时候要如何处理呢?
  3. 将 DB 数据写入到 Excel 并下载,如何实现呢?
  4. 从 EasyExcel 的设计上,你学到了什么,欢迎博客下方留言讨论

推荐阅读

--------

趣味原创解析Java技术栈问题,将复杂问题简单化,将抽象问题图形化落地

如果对我的专题内容感兴趣,或抢先看更多内容,欢迎访问我的博客

1.《【怎么查看excel内存】方便灵活地读取EasyExcel Excel内容》援引自互联网,旨在传递更多网络信息知识,仅代表作者本人观点,与本网站无关,侵删请联系页脚下方联系方式。

2.《【怎么查看excel内存】方便灵活地读取EasyExcel Excel内容》仅供读者参考,本网站未对该内容进行证实,对其原创性、真实性、完整性、及时性不作任何保证。

3.文章转载时请保留本站内容来源地址,https://www.lu-xu.com/keji/2507516.html