一文带你彻底玩转EasyExcel:导入导出excel数据起飞 您所在的位置:网站首页 计算最小值要用到哪个函数公式 一文带你彻底玩转EasyExcel:导入导出excel数据起飞

一文带你彻底玩转EasyExcel:导入导出excel数据起飞

2024-06-19 01:32| 来源: 网络整理| 查看: 265

1.简介

在日常的开发工作中,Excel 文件的读取和写入是非常常见的需求,特别是在后台管理系统中更为频繁,基于传统的方式操作excel通常比较繁琐,EasyExcel 库的出现为我们带来了更简单、更高效的解决方案。本文将介绍 EasyExcel 库的基本用法和一些常见应用场景,帮助开发者更好地利用 EasyExcel 提高工作效率。官网地址:https://github.com/alibaba/easyexcel

青出于蓝而胜于蓝

Java解析、生成Excel比较有名的框架有Apache poi、jxl。但他们都存在一个严重的问题就是非常的耗内存,poi有一套SAX模式的API可以一定程度的解决一些内存溢出的问题,但POI还是有一些缺陷,比如07版Excel解压缩以及解压后存储都是在内存中完成的,内存消耗依然很大。 easyexcel重写了poi对07版Excel的解析,一个3M的excel用POI sax解析依然需要100M左右内存,改用easyexcel可以降低到几M,并且再大的excel也不会出现内存溢出;03版依赖POI的sax模式,在上层做了模型转换的封装,让使用者更加简单方便

16M内存23秒读取75M(46W行25列)的Excel(3.2.1+版本)

当然还有极速模式能更快,但是内存占用会在100M多一点 !

EasyExcel 的核心类主要包括以下几个:

ExcelReader: 用于读取 Excel 文件的核心类。通过 ExcelReader 类可以读取 Excel 文件中的数据,并进行相应的处理和操作。ExcelWriter: 用于写入 Excel 文件的核心类。通过 ExcelWriter 类可以将数据写入到 Excel 文件中,并进行样式设置、标题添加等操作。AnalysisEventListener: 事件监听器接口,用于处理 Excel 文件读取过程中的事件,如读取到每一行数据时的操作。AnalysisContext: 读取 Excel 文件时的上下文信息,包括当前行号、sheet 名称等。通过 AnalysisContext 可以获取到读取过程中的一些关键信息。WriteHandler: 写入 Excel 文件时的处理器接口,用于处理 Excel 文件的样式设置、标题添加等操作。WriteSheet: 写入 Excel 文件时的 Sheet 配置类,用于指定写入数据的 Sheet 名称、样式等信息。

这些核心类在 EasyExcel 中承担了不同的角色,协作完成了 Excel 文件的读取和写入操作。开发者可以根据具体的需求和场景,使用这些类来实现 Excel 文件的各种操作。

Alibaba EasyExcel的核心入口类是EasyExcel类,就想我们平时封装的Util类一样,通过它对excel数据读取或者导出。

2.EasyExcel数据导入 2.1.简单导入

准备excel数据文件

这里以用户信息数据为例

定义用户信息User类

@Builder @AllArgsConstructor @NoArgsConstructor @Data public class User { private Long id; private String userNo; private String name; private Integer gender; private Date birthday; private String phone; private String email; @ExcelIgnore private Integer isDelete; private String address; }

事件监听器ReadListener

我们一般使用EasyExcel 的 AnalysisEventListener,用于监听 Excel 文件读取事件的接口,通过实现这个接口,可以在读取 Excel 文件的过程中对数据进行处理和操作:

/** * @author fjzheng * @version 1.0 * @date 2024/4/12 16:22 */ @Slf4j public class UserExcelListener extends AnalysisEventListener { /** * 每隔100条处理下,然后清理list ,方便内存回收 */ private static final int BATCH_COUNT = 100; /** * 缓存的数据 */ private List cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT); /** * * @param exception * @param context * @throws Exception */ @Override public void onException(Exception exception, AnalysisContext context) throws Exception { log.error("======>>>解析异常:", exception); throw exception; } /** * 当读取到一行数据时,会调用这个方法,并将读取到的数据以及上下文信息作为参数传入 * 可以在这个方法中对读取到的数据进行处理和操作,处理数据时要注意异常错误,保证读取数据的稳定性 * @param user * @param context */ @Override public void invoke(User user, AnalysisContext context) { log.info("解析到一条数据:{}", user); cachedDataList.add(user); if (cachedDataList.size() >= BATCH_COUNT) { // 处理缓存的数据,比如说入库。。。 // 然后清空 cachedDataList.clear(); } } /** * 当每个sheet所有数据读取完毕后,会调用这个方法,可以在这个方法中进行一些收尾工作,如资源释放、数据汇总等。 * @param context */ @Override public void doAfterAllAnalysed(AnalysisContext context) { // 收尾工作,处理剩下的缓存数据。。。 log.info("sheet={} 所有数据解析完成!", context.readSheetHolder().getSheetName()); } }

注意 UserExcelListener 不能被Spring管理,要每次读取excel都要new一个新的监听器,如果里面用到spring bean,可以通过构造方法传进去

执行测试用例

/** * @author fjzheng * @version 1.0 * @date 2024/4/11 18:04 */ @SpringBootTest @RunWith(SpringRunner.class) @Slf4j public class ExcelTest { @Test public void testExcelRead() { String fileName = "/Users/shepherdmy/Desktop/test1.xlsx"; EasyExcel.read(fileName, User.class, new UserExcelListener()).sheet().doRead(); // 如果excel是多行表头比如说2行,需要设置行头数headRowNumber,默认不设置为1行表头,sheet不传默认读取第一个sheet // EasyExcel.read(fileName, User.class, new UserExcelListener()).sheet().headRowNumber(2).doRead(); } }

执行结果控制台输出如下:

解析到一条数据:User(id=1, userNo=she001, name=王小二, gender=0, birthday=Fri Apr 12 10:00:00 CST 2024, phone=123456789, [email protected], isDelete=null, address=杭州市余杭区未来科技城) 解析到一条数据:User(id=2, userNo=she002, name=张三, gender=1, birthday=Wed Apr 10 18:00:00 CST 2024, phone=987654321, [email protected], isDelete=null, address=杭州市拱墅区城西银泰) 解析到一条数据:User(id=3, userNo=she003, name=李四, gender=1, birthday=Thu Apr 11 18:00:00 CST 2024, phone=345686789, [email protected], isDelete=null, address=上海陆家嘴浦东) sheet=用户信息 所有数据解析完成!

完美解析导入,你是否注意到上面我们定义User类的属性和excel文件的字段一一对应,顺序高度一致,如果顺序映射不一致,比如说定义的时候把姓名name和性别gender的字段顺序调换,会导致导入解析字符串类型转整数失败,这是因为在 EasyExcel 中,当导入数据时,默认情况下是按照 Excel 表格中列的顺序来映射数据的。也就是说,如果 Excel 表格的第一列对应 Java 对象的第一个属性,第二列对应第二个属性,以此类推。

在源码层面,EasyExcel 是通过反射机制来实现数据的映射的。具体来说,EasyExcel 在读取 Excel 文件时,会根据 Java 对象的属性顺序和 Excel 表格中列的顺序来一一对应。这个过程是在 EasyExcel 内部进行的,开发者不需要关心具体的实现细节,只需要提供正确的 Java 对象结构和 Excel 表格格式即可。

项目推荐:基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba企业级系统架构底层框架封装,解决业务开发时常见的非功能性需求,防止重复造轮子,方便业务快速开发和企业技术栈框架统一管理。引入组件化的思想实现高内聚低耦合并且高度可配置化,做到可插拔。严格控制包依赖和统一版本管理,做到最少化依赖。注重代码规范和注释,非常适合个人学习和企业使用

Github地址:https://github.com/plasticene/plasticene-boot-starter-parent

Gitee地址:https://gitee.com/plasticene3/plasticene-boot-starter-parent

微信公众号:Shepherd进阶笔记

交流探讨qun:Shepherd_126

2.2 EasyExcel提供的注解

上面User类的定义使用了注解@ExcelIgnore,表示字段isDelete不参与数据映射,要不然严格按照顺序就会把excel的地址列映射给isDelete导致字符串转整数类型转换错误了。

EasyExcel 提供了一些注解,用于帮助开发者更灵活地控制 Excel 文件的读取和写入操作。以下是 EasyExcel 中常用的注解:

@ExcelProperty

@ExcelProperty 注解用于标注 Java 对象中的属性与 Excel 表格中的列的对应关系。该注解包含以下属性:

value:指定 Excel 表格中的列索引,表示该属性对应的列在 Excel 表格中的位置。支持字符串形式(如 “A”、“B”、“C”)和数字形式(从 0 开始)。index:同 value,用于指定 Excel 表格中的列索引。index 和 value 二选一即可,用于指定 Excel 表格中的列索引。

示例:

@Data @Builder @AllArgsConstructor @NoArgsConstructor @TableName(value = "tb_user") public class User { @TableId(type = IdType.AUTO) @ExcelProperty(index = 0) private Long id; @ExcelProperty(index = 1) private String userNo; @ExcelProperty(index = 3) // @ExcelProperty("性别") private Integer gender; @ExcelProperty(index = 2) // @ExcelProperty("姓名") private String name; @ExcelProperty(index = 4) private Date birthday; @ExcelProperty(index = 5) private String phone; @ExcelProperty(index = 6) private String email; // @ExcelIgnore private Integer isDelete; @ExcelProperty(index = 7) private String address; }

一开始我们定义User类时并没有使用@ExcelProperty(),但其实等价于按照类属性顺序从0开始加上了@ExcelProperty(index =列顺序号),注意我特意把字段属性姓名name和性别gender调换了顺序,但是index是按照excel来写的,同时我去掉了isDelete的忽略属性@ExcelIgnore。正常解析导入。。。

在我们日常开发过程中,我个人建议在excel映射的类上必须使用@ExcelProperty()注解标注,至于使用@ExcelProperty(index = 2)还是@ExcelProperty("姓名")都可以,但不能同时用,我个人倾向于@ExcelProperty("姓名"),这样可以备注字段含义,当然一般人不太会修改excel表头的字段,但是很有可能因为显示查看会拖动excel列的顺序这样就会导致@ExcelProperty(index =列顺序号)映射出错。

@ExcelIgnore

@ExcelIgnore 注解用于标注 Java 对象中的属性,表示在 Excel 文件的读取和写入过程中忽略该属性。示例如上描述

@ExcelIgnoreUnannotated

@ExcelIgnoreUnannotated 注解用于指定在读取 Excel 文件时是否忽略未标注 @ExcelProperty 注解的属性,默认为 false,即不忽略。上面我们去掉了isDelete的@ExcelIgnore能正常导出那是因为我们excel文件只有8列刚刚好够标注了@ExcelProperty映射,加入我们excel文件在添加一列,就会去映射isDelete,不符合我们预期,这时候我们就需要在类使用**@ExcelIgnoreUnannotated**解决此问题

@ExcelSheet

@ExcelSheet 注解用于指定读取 Excel 文件时的 Sheet 名称。通常用于读取多个 Sheet 的情况。

@ExcelIgnoreRowNum

@ExcelIgnoreRowNum 注解用于标注 Java 对象中的属性,表示在 Excel 文件的读取过程中忽略行号。通常用于读取时不需要关注行号的情况。

@ColumnWidth

@ColumnWidth主要是控制列宽

2.1.3 日期、数字或者自定义格式转换

数据转换我们只需要实现easyexcel封装的Converter接口即可,比如在上面的用户信息类User的学号属性userNo统一在excel的值加上前缀uno:, 性别属性gender由字符串转枚举值男:0 女:1 未知:2

UserNoConverter

public class UserNoConverter implements Converter { /** * 支持的java类型 * @return */ @Override public Class supportJavaTypeKey() { return String.class; } /** * 支持的excel值类型 * @return */ @Override public CellDataTypeEnum supportExcelTypeKey() { return CellDataTypeEnum.STRING; } /** * 这里读的时候会调用 * * @param context * @return */ @Override public String convertToJavaData(ReadConverterContext context) { return "uno:" + context.getReadCellData().getStringValue(); } /** * 这里是写的时候会调用 不用管 * * @return */ @Override public WriteCellData convertToExcelData(WriteConverterContext context) { return new WriteCellData(context.getValue()); } }

GenderConverter

public class GenderConverter implements Converter { @Override public Class supportJavaTypeKey() { return Integer.class; } @Override public CellDataTypeEnum supportExcelTypeKey() { return CellDataTypeEnum.STRING; } /** * 从excel读数据时候调用 * @param cellData * @param contentProperty * @param globalConfiguration * @return */ @Override public Integer convertToJavaData(ReadCellData cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { String value = cellData.getStringValue(); if (StringUtils.isBlank(value)){ // 未知 return 2; } if (value.indexOf('男') != -1) { return 0; } if (value.indexOf('女') != -1) { return 1; } return 2; } /** * 写数据到excel里面 * @param context * @return */ @Override public WriteCellData convertToExcelData(WriteConverterContext context) { Integer value = context.getValue(); if (Objects.equals(value, 0)) { return new WriteCellData("男"); } if (Objects.equals(value, 1)) { return new WriteCellData("女"); } return new WriteCellData("未知"); } }

在映射的实体类User进行转换器绑定:

@Data @Builder @AllArgsConstructor @NoArgsConstructor // @ExcelIgnoreUnannotated public class User { @ExcelProperty(index = 0) private Long id; @ExcelProperty(index = 1, converter = UserNoConverter.class) private String userNo; @ExcelProperty(index = 3, converter = GenderConverter.class) // @ExcelProperty("性别") private Integer gender; @ExcelProperty(index = 2) // @ExcelProperty("姓名") private String name; @ExcelProperty(index = 4) private Date birthday; @ExcelProperty(index = 5) private String phone; @ExcelProperty(index = 6) private String email; @ExcelIgnore private Integer isDelete; @ExcelProperty(index = 7) private String address; }

执行结果:

2.1.4 读取多个sheet

我们前面用户信息基础再加了一个sheet2接着存用户信息,同时新增一个sheet=银行信息存储银行账户信息:

银行账号信息类:

@Data public class Account { private String name; private String idCard; private String cardNo; private BigDecimal amount; private Integer status; private Date lastUsedTime; }

如果我们没有银行信息这个sheet,我们可以用如下代码读写所有sheet:因为每个sheet的数据格式是一样的

// 读取所有sheet EasyExcel.read(fileName, User.class, new UserExcelListener()).doReadAll();

每个sheet的数据格式不一样,只能单独一一处理,新建一个处理银行账号信息的监听器:

@Slf4j public class AccountExcelListener extends AnalysisEventListener { @Override public void onException(Exception exception, AnalysisContext context) throws Exception { log.error("======>>>解析异常:", exception); } @Override public void invoke(Account data, AnalysisContext context) { log.info("解析到一条数据:{}", data); } @Override public void doAfterAllAnalysed(AnalysisContext context) { log.info("sheet={} 所有数据解析完成!", context.readSheetHolder().getSheetName()); } /** * 解析表头数据 * @param headMap * @param context */ @Override public void invokeHead(Map headMap, AnalysisContext context) { log.info("表头数据:{}", ConverterUtils.convertToStringMap(headMap, context)); } }

测试用例方法改为如下:

@Test public void testExcelRead() { String fileName = "/Users/shepherdmy/Desktop/testExcel.xlsx"; // 读取用户信息两个sheet EasyExcel.read(fileName, User.class, new UserExcelListener()).sheet(0).doRead(); EasyExcel.read(fileName, User.class, new UserExcelListener()).sheet(1).doRead(); // 读取银行账户信息 EasyExcel.read(fileName, Account.class, new AccountExcelListener()).sheet(2).doRead(); }

执行结果:

解析到一条数据:User(id=1, userNo=uno:she001, gender=2, name=王小二, birthday=Fri Apr 12 10:00:00 CST 2024, phone=123456789, [email protected], isDelete=null, address=杭州市余杭区未来科技城) 解析到一条数据:User(id=2, userNo=uno:she002, gender=2, name=张三, birthday=Wed Apr 10 18:00:00 CST 2024, phone=987654321, [email protected], isDelete=null, address=杭州市拱墅区城西银泰) 解析到一条数据:User(id=3, userNo=uno:she003, gender=2, name=李四, birthday=Thu Apr 11 18:00:00 CST 2024, phone=345686789, [email protected], isDelete=null, address=上海陆家嘴浦东) sheet=用户信息 所有数据解析完成! 解析到一条数据:User(id=5, userNo=uno:she005, gender=2, name=小五, birthday=Fri Apr 12 10:00:00 CST 2024, phone=123456789, [email protected], isDelete=null, address=成都太古里) 解析到一条数据:User(id=6, userNo=uno:she006, gender=2, name=小六, birthday=Wed Apr 10 18:00:00 CST 2024, phone=987654321, [email protected], isDelete=null, address=上海南京路) 解析到一条数据:User(id=7, userNo=uno:she007, gender=2, name=小七, birthday=Thu Apr 11 18:00:00 CST 2024, phone=345686789, [email protected], isDelete=null, address=杭州湖滨路) sheet=用户信息2 所有数据解析完成! 表头数据:{0=姓名, 1=身份证号, 2=银行卡号, 3=余额, 4=状态, 5=最近使用时间} 解析到一条数据:Account(name=张三, idCard=3322114566, cardNo=62023445556, amount=10000.88, status=0, lastUsedTime=Sun Dec 31 00:00:00 CST 2023) 解析到一条数据:Account(name=李四, idCard=53134557, cardNo=623454576878, amount=6666.66, status=1, lastUsedTime=Sun Jun 11 00:00:00 CST 2023) sheet=银行信息 所有数据解析完成! 3.EasyExcel数据导出

使用EasyExcel数据导出相对来说很简单,直接上代码,先来看看用户信息类User

@Data @Builder @AllArgsConstructor @NoArgsConstructor public class User { @ExcelProperty(index = 0) private Long id; @ExcelProperty(index = 1, converter = UserNoConverter.class) private String userNo; @ExcelProperty(value = "性别", converter = GenderConverter.class) private Integer gender; @ExcelProperty(value = "姓名") private String name; @ExcelProperty(index = 4) private Date birthday; @ExcelProperty(index = 5) private String phone; @ExcelProperty(index = 6) private String email; @ExcelIgnore private Integer isDelete; @ExcelProperty(index = 7) private String address; }

我们前面说过使用@ExcelProperty时,不建议同时使用index和value去映射属性,我这里这么写只是让你看看导出效果来突出使用value@ExcelProperty(value = "姓名")的好处,话不多说直接看测试用例,我造了10条用户信息数据导出:

@Test public void testExcelWrite() { List userList = new ArrayList(); int i = 0; while (i List userList = new ArrayList(); int i = 0; while (i // int start = k * 3; // int end = (k + 1) * 3 >= size ? size : (k + 1) * 3; // // 这里其实应该是分页查询数据,下面只是简单模拟下 // List users = userList.subList(start, end); // excelWriter.write(users, writeSheet); // } // // 关闭io才会写入数据,必须执行,所以最好用try-with-source安全,如下 // excelWriter.finish(); // 官方安全写法 try (ExcelWriter excelWriter = EasyExcel.write(fileName, User.class).build()) { // 这里注意 如果同一个sheet只要创建一次 WriteSheet writeSheet = EasyExcel.writerSheet("模板").build(); // 去调用写入,实际使用时根据数据库分页的总的页数来 for (int k = 0; k List heads = ListUtils.newArrayList("员工编号", "姓名", "性别", "手机号", "金额"); // 表头 List dataList = new ArrayList(); dataList.add(heads); int i = 0; while (i


【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有