java:EasyExcel使用(一)读excel

ops/2024/11/14 12:47:03/

javaEasyExcelexcel_0">java:EasyExcel使用(一)读excel

1 前言

EasyExcel相比于传统使用poi进行excel文件读写,编程使用操作上更加方便快捷,且对于内存溢出进行了优化处理。本文是EasyExcel读excel操作。

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模式,在上层做了模型转换的封装,让使用者更加简单方便。

EasyExcel官方文档:

java">https://easyexcel.opensource.alibaba.com/

进入maven.org,自动跳转如下进行maven仓库查询:

java">https://central.sonatype.com/?smo=true

搜索easyexcel:

在这里插入图片描述

pom文件添加如下依赖:

<dependency><groupId>com.alibaba</groupId><artifactId>easyexcel</artifactId><version>3.3.4</version>
</dependency>

2 使用(Excel读取)

2.1 读Excel

GoodsDto:

java">package com.xiaoxu.excel.readE;import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;import java.util.Date;/*** @author xiaoxu* @date 2024-04-26 22:32* learn_java:com.xiaoxu.excel.readE.GoodsDto*/
@Getter
@Setter
@EqualsAndHashCode
public class GoodsDto {private String name;private Date date;private long count;private Date produceDate;private double price;
}

在桌面准备的excel文件,test.xlsx:

在这里插入图片描述

excel读取方式一:

TestExcelRead,读取excel(JDK8.0+,不用额外写ReadListener,since: 3.0.0-beta1):

java">package com.xiaoxu.excel.readE;import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.read.listener.PageReadListener;
import com.alibaba.fastjson.JSON;import javax.swing.filechooser.FileSystemView;
import java.io.File;
import java.text.SimpleDateFormat;/*** @author xiaoxu* @date 2024-04-27 14:53* learn_java:com.xiaoxu.excel.readE.TestExcelRead*/
public class TestExcelRead {public static void main(String[] args) {// C:\Users\****\DesktopFile homeDirectory = FileSystemView.getFileSystemView().getHomeDirectory();String fileName = "test";//  读取桌面路径excel文件String file = homeDirectory.getAbsolutePath()+ File.separator + fileName + ".xlsx";EasyExcel.read(file, GoodsDto.class,new PageReadListener<GoodsDto>(dataList -> {for (GoodsDto goodsDto : dataList) {System.out.println("读取到excel数据:" +JSON.toJSONString(goodsDto));SimpleDateFormat sdf =new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");if (null != goodsDto.getDate()) {System.out.println("日期:" +sdf.format(goodsDto.getDate()));}if (null != goodsDto.getProduceDate()) {System.out.println("生产日期:" +sdf.format(goodsDto.getProduceDate()));}}})).sheet().doRead();}}

执行excel读取结果如下:

java">读取到excel数据:
{"count":45,"date":1709099280000,
"name":"苹果","price":1.65,"produceDate":15278342400000}
日期:2024-02-28 13:48:00.000
生产日期:2454-02-25 00:00:00.000读取到excel数据:{"count":13,"date":1713611400000,
"name":"梨子","price":2.4,"produceDate":15278515200000}
日期:2024-04-20 19:10:00.000
生产日期:2454-02-27 00:00:00.000读取到excel数据:{"count":0,"name":"菠萝","price":9.9}

日期转换说明,需要按照excel的日期格式来写,

上述结果表明,如果是引用类型,那么excel的cell值为null时,不会为实体类赋值;如果实体类是primitive类型数据,同时excel的cell值为null时,会将null转换成该primitive的默认值,比如实体类的count属性为long时,默认值就是0,所以也会赋值(因为包装类的值为null时,直接转换成primitive类型值时,不可以为其赋值为null,必须转换成其对应的默认值,否则将会抛出异常)。

同时注意,上述的实体类的字段,和excel的每一列是按顺序一一对应定义的。否则可能会抛出ExcelDataConvertException转换异常(Excel的值类型需要和实体类字段类型一一对应)。

excel读取方式二:

TestExcelRead2,读取excel(定义监听器GoodsDtoListener读取excel):

GoodsDtoListener:

java">package com.xiaoxu.excel.readE;import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.read.listener.ReadListener;
import com.alibaba.excel.util.ListUtils;
import com.alibaba.fastjson.JSON;import java.util.List;/*** @author xiaoxu* @date 2024-04-26 22:42* learn_java:com.xiaoxu.excel.readE.GoodsDtoListener*/
public class GoodsDtoListener implements ReadListener<GoodsDto> {/*** 每隔5条存储数据库,实际使用中可以100条,然后清理list ,方便内存回收*/private static final int BATCH_COUNT = 100;/*** 缓存数据*/private List<GoodsDto> cache = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);/*** 假设这个是一个DAO,* 当然有业务逻辑这个也可以是一个service。* 当然如果不用存储这个对象没用。*/private GoodsDAO goodsDAO;public GoodsDtoListener() {//  这里是demo,所以随便new一个。//  实际使用如果到了spring,请使用下面的有参构造函数this.goodsDAO = new GoodsDAO();}public GoodsDtoListener(GoodsDAO goodsDAO) {//  如果使用了spring,请使用这个构造方法。//  每次创建Listener的时候需要把spring管理的类传进来this.goodsDAO = goodsDAO;}/*** @param goodsDto        one row value.*                        Is is same*                        as {@link AnalysisContext#readRowHolder()}* @param analysisContext 这个每一条数据解析都会来调用*/@Overridepublic void invoke(GoodsDto goodsDto, AnalysisContext analysisContext) {System.out.println(String.format("解析到数据: %s.", JSON.toJSONString(goodsDto)));cache.add(goodsDto);// 达到BATCH_COUNT了,需要去存储一次数据库,// 防止数据几万条数据在内存,容易OOMif (cache.size() >= BATCH_COUNT) {saveData();// cache置为空List,方便gc workcache = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);}}private void saveData() {System.out.println("开始存储数据数数据:" + cache.size());goodsDAO.save(cache);}/*** @param analysisContext 所有数据处理完,调用这里*/@Overridepublic void doAfterAllAnalysed(AnalysisContext analysisContext) {//  这里也要保存数据,//  确保最后遗留的数据也存储到数据库System.out.println("最终数据存储:");saveData();System.out.println("所有数据解析完成!");}}

GoodsDAO :

java">public class GoodsDAO {public void save(List<GoodsDto> goodsDtoList) {// 如果是mybatis,尽量别直接调用多次insert,// 自己写一个mapper里面新增一个方法batchInsert,// 所有数据一次性插入// batchInsert的插入性能更好System.out.println("存储数据成功~");}}

TestExcelRead2:

java">package com.xiaoxu.excel.readE;import com.alibaba.excel.EasyExcel;import javax.swing.filechooser.FileSystemView;
import java.io.File;/*** @author xiaoxu* @date 2024-04-27 16:37* learn_java:com.xiaoxu.excel.readE.TestExcelRead2*/
public class TestExcelRead2 {public static void main(String[] args) {// C:\Users\****\DesktopFile homeDirectory = FileSystemView.getFileSystemView().getHomeDirectory();String fileName = "test";//  读取桌面路径excel文件String file = homeDirectory.getAbsolutePath()+ File.separator + fileName + ".xlsx";//   有个很重要的点 GoodsDtoListener 不能被spring管理,//   要每次读取excel都要new,然后里面用到spring可以构造方法传进去//   excel有多个Sheet,可以通过 .sheet("Sheet1")指定sheet名称//   或者可以通过.sheet(0)指定读取第1个sheet   //   如果sheet名称不存在,那么不会报错,也不会读取到数据EasyExcel.read(file, GoodsDto.class, new GoodsDtoListener()).sheet("Sheet1").doRead();}}

执行结果如下:

java">解析到数据: {"count":45,"date":1709099280000,
"name":"苹果","price":1.65,"produceDate":1746545644800000}.
解析到数据: {"count":13,"date":1713611400000,
"name":"梨子","price":2.4,"produceDate":1746563788800000}.
解析到数据: {"count":0,"name":"菠萝","price":9.9}.
最终数据存储:
开始存储数据数数据:3
存储数据成功~
所有数据解析完成!

注意:excel文件有多个sheet时,.sheet()表示读取第1个sheet的内容。若需要指定sheet名称读取,则使用.sheet(“Sheet1”)即可指定名称为Sheet1的sheet进行读取,若不知道sheet名称,那么可以通过.sheet(0)读取第1个sheet,.sheet(1)读取第2个sheet等等的方式来进行读取,下标从0开始。如果sheet的名称不存在,或者sheet的索引下标不存在(比如总共只有3个sheet,但是读取为.sheet(3)第4个sheet)时,不会抛出异常,只是无法读取到对应数据。并且EasyExcel读取sheet的数据时,表头不会进入Listener回调中,而是从第二行的内容数据开始进入回调。

excel读取方式三:

TestExcelRead3,读取excel(通过ExcelReader、ReadSheet读取excel文件):

java">package com.xiaoxu.excel.readE;import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelReader;
import com.alibaba.excel.read.metadata.ReadSheet;import javax.swing.filechooser.FileSystemView;
import java.io.File;/*** @author xiaoxu* @date 2024-04-27 19:32* learn_java:com.xiaoxu.excel.readE.TestExcelRead3*/
public class TestExcelRead3 {public static void main(String[] args) {// C:\Users\****\DesktopFile homeDirectory = FileSystemView.getFileSystemView().getHomeDirectory();String fileName = "test";//  读取桌面路径excel文件String file = homeDirectory.getAbsolutePath()+ File.separator + fileName + ".xlsx";// 一个文件一个readertry (ExcelReader excelReader = EasyExcel.read(file, GoodsDto.class, new GoodsDtoListener()).build()) {// 构建一个sheet 这里可以指定名字或者noReadSheet readSheet = EasyExcel.readSheet(0).build();// 读取一个sheetexcelReader.read(readSheet);}}}

读取结果同读取方式二一致。

2.2 指定列的下标或者列名

前面提到了,实体类的字段必须按照顺序和excel的列一一对应,否则可能出现转换异常或者赋值与实际字段不匹配的情况。但是这种情况也有解决方式,即使用EasyExcel提供的@ExcelProperty注解

GoodsDtoProperty:

java">package com.xiaoxu.excel.cols;import com.alibaba.excel.annotation.ExcelProperty;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;import java.util.Date;/*** @author xiaoxu* @date 2024-04-27 20:36* learn_java:com.xiaoxu.excel.cols.GoodsDtoPropertty*/
@Getter
@Setter
@EqualsAndHashCode
public class GoodsDtoProperty {//  注解的默认值是value,这里表示excel的表头的列名称@ExcelProperty("售卖日期")private Date date;@ExcelProperty(index = 99)private long count;private Date produceDate;// index表示读取第几列,0表示第一列,1表示第二列...// 这里price属性是实体类第四个属性,没有 @ExcelProperty表示和第4列对应// 这里表示和excel的第5列对应@ExcelProperty(index = 4)private double price;@ExcelProperty(value = "名称")private String name;
}

excel数据如下:

在这里插入图片描述

TestExcelReadProp:

java">package com.xiaoxu.excel.cols;import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.read.listener.PageReadListener;
import com.alibaba.fastjson.JSON;import javax.swing.filechooser.FileSystemView;
import java.io.File;
import java.text.SimpleDateFormat;/*** @author xiaoxu* @date 2024-04-27 20:46* learn_java:com.xiaoxu.excel.cols.TestExcelReadProp*/
public class TestExcelReadProp {public static void main(String[] args) {// C:\Users\****\DesktopFile homeDirectory = FileSystemView.getFileSystemView().getHomeDirectory();String fileName = "test";//  读取桌面路径excel文件String file = homeDirectory.getAbsolutePath()+ File.separator + fileName + ".xlsx";EasyExcel.read(file, GoodsDtoProperty.class,new PageReadListener<GoodsDtoProperty>(dataList -> {for (GoodsDtoProperty goodsDtoProp : dataList) {System.out.println("读取到excel数据:" +JSON.toJSONString(goodsDtoProp));SimpleDateFormat sdf =new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");if (null != goodsDtoProp.getDate()) {System.out.println("日期:" +sdf.format(goodsDtoProp.getDate()));}if (null != goodsDtoProp.getProduceDate()) {System.out.println("生产日期:" +sdf.format(goodsDtoProp.getProduceDate()));}}})).sheet().doRead();}}

执行结果:

javascript">读取到excel数据:{"count":0,"name":"苹果",
"price":1.65,"produceDate":1709099280000}
生产日期:2024-02-28 13:48:00.000读取到excel数据:{"count":0,"name":"梨子",
"price":2.4,"produceDate":1713611400000}
生产日期:2024-04-20 19:10:00.000读取到excel数据:{"count":0,"name":"菠萝","price":9.9}

上述表明,如果实体类中使用了@ExcelProperty注解,那么最好是全部属性都加上,这样方便和excel需要的属性列一一对应,否则可能出现部分值胡乱匹配excel列值的情况。同时,如果注解的value值随意填写,即excel的匹配列名不存在,或者index超越索引上限,都不会报错。同时index索引也是从0开始,0表示excel的第1列,1表示excel的第2列,以此类推。

2.3 读多个sheet

2.3.1 读取全部sheet

准备excel文件中存在多个Sheet,文件为test3.xlsx:

Sheet1:

在这里插入图片描述

Sheet2:

在这里插入图片描述

提取读取桌面文件工具类:

java">package com.xiaoxu.excel;import javax.swing.filechooser.FileSystemView;
import java.io.File;/*** @author xiaoxu* @date 2024-04-27 22:00* learn_java:com.xiaoxu.excel.DeskTool*/
public class DeskTool {public static String getPath(String fileName) {// C:\Users\****\DesktopFile homeDirectory = FileSystemView.getFileSystemView().getHomeDirectory();//  读取桌面路径excel文件return homeDirectory.getAbsolutePath()+ File.separator + fileName + ".xlsx";}}

GoodsDtoNew:

java">package com.xiaoxu.excel.multiSht;import com.alibaba.excel.annotation.ExcelProperty;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;import java.util.Date;/*** @author xiaoxu* @date 2024-04-27 22:17* learn_java:com.xiaoxu.excel.multiSht.GoodsDtoNew*/
@Getter
@Setter
@EqualsAndHashCode
public class GoodsDtoNew {//  注解的默认值是value,这里表示excel的表头的列名称@ExcelProperty("日期")private Date date;@ExcelProperty("生产日期")private Date produceDate;@ExcelProperty("价格")private double price;@ExcelProperty("数量")private long count;@ExcelProperty(value = "名称")private String name;
}

GoodsDtoNewListener:

java">package com.xiaoxu.excel.multiSht;import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.read.listener.ReadListener;
import com.alibaba.fastjson.JSON;/*** @author xiaoxu* @date 2024-04-27 22:20* learn_java:com.xiaoxu.excel.multiSht.GoodsDtoNewListener*/
public class GoodsDtoNewListener implements ReadListener<GoodsDtoNew> {@Overridepublic void invoke(GoodsDtoNew goodsDtoNew, AnalysisContext analysisContext) {System.out.println("扫描数据:"+ JSON.toJSONString(goodsDtoNew));}@Overridepublic void doAfterAllAnalysed(AnalysisContext analysisContext) {System.out.println("所有数据解析完成!");}
}

TestExcelReadMultiSheetAll:

java">package com.xiaoxu.excel.multiSht;import com.alibaba.excel.EasyExcel;
import com.xiaoxu.excel.DeskTool;/*** @author xiaoxu* @date 2024-04-27 21:57* learn_java:com.xiaoxu.excel.multiSht.TestExcelReadMultiSheetAll*/
public class TestExcelReadMultiSheetAll {public static void main(String[] args) {String file = DeskTool.getPath("test3");// 读取全部sheet// 这里需要注意 GoodsDtoNewListener的doAfterAllAnalysed// 会在每个sheet读取完毕后调用一次。// 然后所有sheet都会往同一个GoodsDtoNewListener里面写(或者读)EasyExcel.read(file,GoodsDtoNew.class,new GoodsDtoNewListener()).doReadAll();}}

执行结果:

java">扫描数据:{"count":45,"date":1709099280000,
"name":"苹果","price":1.65,"produceDate":1746545644800000}
扫描数据:{"count":13,"date":1713611400000,
"name":"梨子","price":2.4,"produceDate":1746563788800000}
扫描数据:{"count":0,"name":"菠萝","price":9.9}
所有数据解析完成!扫描数据:{"count":45,"date":1709349480000,
"name":"水蜜桃","price":3.5,"produceDate":1746535536000000}
扫描数据:{"count":13,"date":1713805800000,
"name":"菠萝蜜","price":9.5,"produceDate":1746546508800000}
扫描数据:{"count":88,"date":1714227300000,
"name":"百香果","price":7.8,"produceDate":1746552816000000}
所有数据解析完成!所有数据解析完成!

上述表明,doReadAll()方法会处理全部的Sheet数据,且全部sheet的数据是同一个listener处理的,并且doAfterAllAnalysed()最终读取完毕的回调方法,3个sheet分别调用了3次(因为上述的excel文件还有一个空的Sheet3)。

2.3.2 读取部分sheet

准备excel文件中存在多个Sheet,多个Sheet结构不同(对应不同监听器),文件为test2.xlsx:

Sheet1:

在这里插入图片描述

Sheet2:

在这里插入图片描述

MerchantDto:

java">package com.xiaoxu.excel.multiSht;import com.alibaba.excel.annotation.ExcelProperty;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;/*** @author xiaoxu* @date 2024-04-28 8:23* learn_java:com.xiaoxu.excel.multiSht.MerchantDto*/
@Getter
@Setter
@EqualsAndHashCode
public class MerchantDto {@ExcelProperty("商户名称")private String merchantName;@ExcelProperty("商户库存")private long merchantInventory;}

MerchantDtoListener:

java">package com.xiaoxu.excel.multiSht;import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.read.listener.ReadListener;
import com.alibaba.fastjson.JSON;/*** @author xiaoxu* @date 2024-04-28 8:25* learn_java:com.xiaoxu.excel.multiSht.MerchantDtoListener*/
public class MerchantDtoListener implements ReadListener<MerchantDto> {@Overridepublic void invoke(MerchantDto merchantDto, AnalysisContext analysisContext) {System.out.println("扫描商户数据:"+ JSON.toJSONString(merchantDto));}@Overridepublic void doAfterAllAnalysed(AnalysisContext analysisContext) {System.out.println("商户所有数据解析完成!");}
}

TestExcelReadMultiSheet:

java">package com.xiaoxu.excel.multiSht;import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelReader;
import com.alibaba.excel.read.metadata.ReadSheet;
import com.xiaoxu.excel.DeskTool;/*** @author xiaoxu* @date 2024-04-27 21:56* learn_java:com.xiaoxu.excel.multiSht.TestExcelReadMultiSheet*/
public class TestExcelReadMultiSheet {public static void main(String[] args) {String file = DeskTool.getPath("test2");// 一个文件一个readertry (ExcelReader excelReader = EasyExcel.read(file).build()) {//  可以注册同样的head和Listener 自己使用功能使用不同的Listener// 构建一个sheet 这里可以指定名字或者noReadSheet readSheet = EasyExcel.readSheet(0).head(GoodsDtoNew.class).registerReadListener(new GoodsDtoNewListener()).build();ReadSheet readSheet2 = EasyExcel.readSheet(1).head(MerchantDto.class).registerReadListener(new MerchantDtoListener()).build();// 这里注意 一定要把sheet1 sheet2 一起传进去,// 不然有个问题就是03版的excel 会读取多次,浪费性能excelReader.read(readSheet, readSheet2);}}}

执行结果:

java">扫描数据:{"count":45,"date":1709099280000,
"name":"苹果","price":1.65,"produceDate":1746545644800000}
扫描数据:{"count":13,"date":1713611400000,
"name":"梨子","price":2.4,"produceDate":1746563788800000}
扫描数据:{"count":0,"name":"菠萝","price":9.9}
所有数据解析完成!扫描商户数据:{"merchantInventory":150,"merchantName":"天天水果"}
扫描商户数据:{"merchantInventory":500,"merchantName":"只爱水果"}
商户所有数据解析完成!

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

test4.xlsx:

在这里插入图片描述

ConverterData:

java">package com.xiaoxu.excel.format;import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.format.DateTimeFormat;
import com.alibaba.excel.annotation.format.NumberFormat;
import lombok.Getter;
import lombok.Setter;/*** @author xiaoxu* @date 2024-04-29 20:01* learn_java:com.xiaoxu.excel.format.ConverterData*/
@Setter
@Getter
public class ConverterData {/*** 定义 转换器*/@ExcelProperty(value = "信息",converter = CustomStringConverter.class)private String message;/*** 这里用string 去接日期才能格式化。接收年月日格式*/@DateTimeFormat("yyyy年MM月dd日HH时mm分ss秒SSS毫秒")@ExcelProperty("日期")private String date;/*** 接收百分比的数字*/@NumberFormat("#.##%")@ExcelProperty("价格")private String price;}

CustomStringConverter,日期、数字或者自定义格式转换:

java">package com.xiaoxu.excel.format;import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.converters.ReadConverterContext;
import com.alibaba.excel.converters.WriteConverterContext;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.data.WriteCellData;/*** @author xiaoxu* @date 2024-04-29 20:05* learn_java:com.xiaoxu.excel.format.CustomStringConverter*/
public class CustomStringConverter implements Converter<String> {@Overridepublic Class<?> supportJavaTypeKey() {return String.class;}@Overridepublic CellDataTypeEnum supportExcelTypeKey() {return CellDataTypeEnum.STRING;}/*** @param context* @return* @throws Exception 读的时候调用*/@Overridepublic String convertToJavaData(ReadConverterContext<?> context) throws Exception {String str = context.getReadCellData().getStringValue();char[] chars = str.toCharArray();int left = 0;int right = chars.length - 1;while (left < right) {char temp = chars[left];chars[left] = chars[right];chars[right] = temp;left++;right--;}return new String(chars);}/*** @param context* @return* @throws Exception 写的时候调用,这里不涉及写*/@Overridepublic WriteCellData<?> convertToExcelData(WriteConverterContext<String> context)throws Exception {return new WriteCellData<>(context.getValue());}
}

TestExcelConverter:

java">public class TestExcelConverter {public static void main(String[] args) {String file = DeskTool.getPath("test4");EasyExcel.read(file, ConverterData.class,// 这里注意 我们也可以registerConverter来指定自定义转换器, // 但是这个转换变成全局了,所有java为string,// excel为string的都会用这个转换器。// 如果就想单个字段使用请使用@ExcelProperty 指定converter// .registerConverter(new CustomStringStringConverter())new PageReadListener<ConverterData>((list) -> {for (ConverterData c : list) {System.out.println("读取到excel数据:" +JSON.toJSONString(c));}})).sheet().doRead();}
}

执行结果:

java">读取到excel数据:{"date":"2024年02月28日13时48分00秒000毫秒",
"message":"ebab好你","price":"980%"}
读取到excel数据:{"date":"2024年04月08日11时20分00秒000毫秒",
"message":"EBABEFAC","price":"570%"}

上述convertToJavaData对String类型数据,进行字符串反转。

上述字符串反转,还可以使用下面的方式,效果一致:

java">String str = context.getReadCellData().getStringValue();
char[] chars = str.toCharArray();
int left = 0;
int right = chars.length - 1;
while (left < right) {chars[left] ^= chars[right];chars[right] ^= chars[left];chars[left] ^= chars[right];left++;right--;
}
return new String(chars);

2.5 多行头

test5.xlsx:

在这里插入图片描述

TestMultiHead:

java">public class TestMultiHead {public static void main(String[] args) {String file = DeskTool.getPath("test5");EasyExcel.read(file, ConverterData.class,new PageReadListener<ConverterData>((list) -> {for (ConverterData c : list) {System.out.println("读取到excel数据:" +JSON.toJSONString(c));}})).sheet().doRead();}}

执行结果:

java">读取到excel数据:{"date":"日期","message":"息信","price":"价格"}
读取到excel数据:{"date":"日期","message":"息信","price":"价格"}
读取到excel数据:{"date":"2024年02月28日13时48分00秒000毫秒",
"message":"ebab好你","price":"980%"}
读取到excel数据:{"date":"2024年04月08日11时20分00秒000毫秒",
"message":"EBABEFAC","price":"570%"}

默认就是如下的.headRowNumber(1),表头一般是1行:

java">String file = DeskTool.getPath("test5");
EasyExcel.read(file, ConverterData.class,new PageReadListener<ConverterData>((list) -> {for (ConverterData c : list) {System.out.println("读取到excel数据:" +JSON.toJSONString(c));}})).sheet().headRowNumber(1).doRead();

如果上述有3行表头,我们希望都去掉,那么修改如下:

java">String file = DeskTool.getPath("test5");
EasyExcel.read(file, ConverterData.class,new PageReadListener<ConverterData>((list) -> {for (ConverterData c : list) {System.out.println("读取到excel数据:" +JSON.toJSONString(c));}})).sheet().headRowNumber(3).doRead();

3行表头场景,希望都去掉表头,使用.headRowNumber(3)即可,执行结果如下:

java">读取到excel数据:{"date":"2024年02月28日13时48分00秒000毫秒",
"message":"ebab好你","price":"980%"}
读取到excel数据:{"date":"2024年04月08日11时20分00秒000毫秒",
"message":"EBABEFAC","price":"570%"}

2.6 同步的返回

test6.xlsx:

在这里插入图片描述

PeopleDto:

java">@Setter
@Getter
public class PeopleDto {@ExcelProperty("名称")private String name;@ExcelProperty("年龄")private int age;}

TestSyncRead:

java">String file = DeskTool.getPath("test6");//  synchronous,同步的返回,不推荐使用,
//  如果数据量大会把数据放到内存里面
List<PeopleDto> peopleDtos = EasyExcel.read(file).head(PeopleDto.class).sheet().doReadSync();
for (PeopleDto p : peopleDtos) {System.out.println("读取到excel数据:" +JSON.toJSONString(p));
}
//  这里 也可以不指定class,返回一个list,
//  然后读取第一个sheet 同步读取会自动finish
List<Map<Integer, String>> listMap = EasyExcel.read(file).sheet().doReadSync();
for (Map<Integer, String> data : listMap) {// 返回每条数据的键值对 表示所在的列 和所在列的值System.out.println("读取到数据:" + JSON.toJSONString(data));
}

执行结果:

java">读取到excel数据:{"age":66,"name":"小徐"}
读取到excel数据:{"age":22,"name":"小李"}读取到数据:{0:"小徐",1:"66"}
读取到数据:{0:"小李",1:"22"}

2.7 读取表头数据

监听器重写invokeHeadMap方法即可:

PeopleDtoListener监听器:

java">import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.metadata.data.ReadCellData;
import com.alibaba.excel.read.listener.ReadListener;
import com.alibaba.excel.util.ConverterUtils;
import com.alibaba.fastjson.JSON;
import com.google.common.collect.Lists;import java.util.List;
import java.util.Map;/*** @author xiaoxu* @date 2024-04-30* java_demo:com.xiaoxu.test.excel.sync.PeopleDtoListener*/
public class PeopleDtoListener implements ReadListener<PeopleDto> {List<PeopleDto> peopleDtos = Lists.newArrayListWithCapacity(2);@Overridepublic void invoke(PeopleDto peopleDto, AnalysisContext analysisContext) {peopleDtos.add(peopleDto);}@Overridepublic void doAfterAllAnalysed(AnalysisContext analysisContext) {}@Overridepublic void invokeHead(Map<Integer, ReadCellData<?>> headMap, AnalysisContext context) {Map<Integer, String> head = ConverterUtils.convertToStringMap(headMap, context);System.out.println("读取到表头数据:" + JSON.toJSONString(head));// 如果想转成成 Map<Integer,String>// 方案1: 不要implements ReadListener 而是 extends AnalysisEventListener// 方案2: 调用 ConverterUtils.convertToStringMap(headMap, context) 自动会转换}
}

excel的head表头数据处理:

java">String file = DeskTool.getPath("test6");//  synchronous,同步的返回,不推荐使用,
//  如果数据量大会把数据放到内存里面
//  表头数据读取      
List<PeopleDto> peopleDtos = EasyExcel.read(file).head(PeopleDto.class).registerReadListener(new PeopleDtoListener()).sheet().doReadSync();
for (PeopleDto p : peopleDtos) {System.out.println("读取到excel数据:" +JSON.toJSONString(p));
}

执行结果:

java">读取到表头数据:{0:"名称",1:"年龄"}
读取到excel数据:{"age":66,"name":"小徐"}
读取到excel数据:{"age":22,"name":"小李"}

2.8 额外信息(批注、超链接、合并单元格信息读取)

since 2.0.0-beta1

在这里插入图片描述

对象:

java">@Getter
@Setter
@EqualsAndHashCode
public class DemoExtraData {private String row1;private String row2;
}

监听器:

java">@Slf4j
public class DemoExtraListener implements ReadListener<DemoExtraData> {@Overridepublic void invoke(DemoExtraData data, AnalysisContext context) {}@Overridepublic void doAfterAllAnalysed(AnalysisContext context) {}@Overridepublic void extra(CellExtra extra, AnalysisContext context) {log.info("读取到了一条额外信息:{}", JSON.toJSONString(extra));switch (extra.getType()) {case COMMENT:log.info("额外信息是批注,在rowIndex:{},columnIndex;{},内容是:{}", extra.getRowIndex(), extra.getColumnIndex(),extra.getText());break;case HYPERLINK:if ("Sheet1!A1".equals(extra.getText())) {log.info("额外信息是超链接,在rowIndex:{},columnIndex;{},内容是:{}", extra.getRowIndex(),extra.getColumnIndex(), extra.getText());} else if ("Sheet2!A1".equals(extra.getText())) {log.info("额外信息是超链接,而且覆盖了一个区间,在firstRowIndex:{},firstColumnIndex;{},lastRowIndex:{},lastColumnIndex:{},"+ "内容是:{}",extra.getFirstRowIndex(), extra.getFirstColumnIndex(), extra.getLastRowIndex(),extra.getLastColumnIndex(), extra.getText());} else {Assert.fail("Unknown hyperlink!");}break;case MERGE:log.info("额外信息是超链接,而且覆盖了一个区间,在firstRowIndex:{},firstColumnIndex;{},lastRowIndex:{},lastColumnIndex:{}",extra.getFirstRowIndex(), extra.getFirstColumnIndex(), extra.getLastRowIndex(),extra.getLastColumnIndex());break;default:}}
}
java">/*** 额外信息(批注、超链接、合并单元格信息读取)* <p>* 由于是流式读取,没法在读取到单元格数据的时候直接读取到额外信息,所以只能最后通知哪些单元格有哪些额外信息** <p>* 1. 创建excel对应的实体对象 参照{@link DemoExtraData}* <p>* 2. 由于默认异步读取excel,所以需要创建excel一行一行的回调监听器,参照{@link DemoExtraListener}* <p>* 3. 直接读即可** @since 2.2.0-beat1*/
@Test
public void extraRead() {String fileName = TestFileUtil.getPath() + "demo" + File.separator + "extra.xlsx";// 这里 需要指定读用哪个class去读,然后读取第一个sheetEasyExcel.read(fileName, DemoExtraData.class, new DemoExtraListener())// 需要读取批注 默认不读取.extraRead(CellExtraTypeEnum.COMMENT)// 需要读取超链接 默认不读取.extraRead(CellExtraTypeEnum.HYPERLINK)// 需要读取合并单元格信息 默认不读取.extraRead(CellExtraTypeEnum.MERGE).sheet().doRead();
}

test7.xlsx:

在这里插入图片描述

ConverterData:

java">@Setter
@Getter
public class ConverterData {/*** 定义 转换器*/@ExcelProperty(value = "信息",converter = CustomStringConverter.class)private String message;/*** 这里用string 去接日期才能格式化。接收年月日格式*/@DateTimeFormat("yyyy年MM月dd日HH时mm分ss秒SSS毫秒")@ExcelProperty("日期")private String date;/*** 接收百分比的数字*/@NumberFormat("#.##%")@ExcelProperty("价格")private String price;}

MergeDataListener:

java">package com.xiaoxu.excel.merge;import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.metadata.CellExtra;
import com.alibaba.excel.read.listener.ReadListener;
import com.alibaba.fastjson.JSON;
import com.xiaoxu.excel.format.ConverterData;import java.text.MessageFormat;/*** @author xiaoxu* @date 2024-04-30 22:13* learn_java:com.xiaoxu.excel.merge.MergeDataListener*/
public class MergeDataListener implements ReadListener<ConverterData> {@Overridepublic void invoke(ConverterData converterData, AnalysisContext analysisContext) {System.out.println("读取正常数据:" +JSON.toJSONString(converterData));}@Overridepublic void doAfterAllAnalysed(AnalysisContext analysisContext) {}@Overridepublic void extra(CellExtra extra, AnalysisContext context) {System.out.println("读取额外数据:" +JSON.toJSONString(extra));switch (extra.getType()) {case MERGE:System.out.println(MessageFormat.format("额外信息是合并单元格," +"覆盖区间:firstRowIndex:{0}," +"firstColumnIndex:{1}," +"lastRowIndex:{2},lastColumnIndex:{3}.",extra.getFirstRowIndex(),extra.getFirstColumnIndex(),extra.getLastRowIndex(),extra.getLastColumnIndex()));}}
}

TestMerge:

java">// 这种写法只会输出正常读取的数据,而不会输出合并的单元格等数据
// 批注、超链接、合并单元格默认不读取
//        EasyExcel.read(file).head(ConverterData.class)
//                .registerReadListener(new MergeDataListener())
//                .sheet().headRowNumber(1).doRead();
EasyExcel.read(file).head(ConverterData.class).registerReadListener(new MergeDataListener()).extraRead(CellExtraTypeEnum.MERGE).sheet().headRowNumber(1).doRead();

执行结果:

java">读取正常数据:{"date":"2024年02月28日13时48分00秒000毫秒",
"message":"ebab好你","price":"980%"}
读取正常数据:{"date":"2024年04月08日11时20分00秒000毫秒",
"message":"EBABEFAC","price":"570%"}读取额外数据:{"columnIndex":0,"firstColumnIndex":0,
"firstRowIndex":2,"lastColumnIndex":0,
"lastRowIndex":3,"rowIndex":2,"type":"MERGE"}
额外信息是合并单元格,覆盖区间:firstRowIndex:2,firstColumnIndex:0,
lastRowIndex:3,lastColumnIndex:0.

2.9 读取公式和单元格类型

test8.xlsx:

在这里插入图片描述

其中金额是公式生成的,是单价*数量计算出来的。

公式操作如下,在第一列数据的金额列下,输入=号,然后选中苹果的单价列,输入*表示乘,再选中苹果的数量列,按下enter即公式配置成功,下拉苹果列的金额,即可自动填充后续列的公式:

在这里插入图片描述

CellDataReadData:

java">package com.xiaoxu.excel.formula;import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.metadata.data.CellData;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;import java.util.Date;/*** @author xiaoxu* @date 2024-05-01 10:22* learn_java:com.xiaoxu.excel.formula.CellDataReadData*/
@Getter
@Setter
@EqualsAndHashCode
public class CellDataReadData {@ExcelProperty("品名")private CellData<String> name;@ExcelProperty("单价")private CellData<Long> price;@ExcelProperty("数量")private CellData<Long> number;@ExcelProperty("金额")private CellData<Long> money;@ExcelProperty("日期")private CellData<Date> date;
}

CellDataListener:

java">public class CellDataListener implements ReadListener<CellDataReadData> {@Overridepublic void invoke(CellDataReadData cellDataReadData, AnalysisContext analysisContext) {System.out.println("读取正常数据:" +JSON.toJSONString(cellDataReadData, true));}@Overridepublic void doAfterAllAnalysed(AnalysisContext analysisContext) {}
}

TestCellData:

java">String file = DeskTool.getPath("test8");
EasyExcel.read(file).head(CellDataReadData.class).registerReadListener(new CellDataListener()).extraRead(CellExtraTypeEnum.MERGE).sheet().headRowNumber(1).doRead();

执行结果:

java">读取正常数据:{"date":{"data":1709099280000,"dataFormatData":{"format":"yyyy-m-d h:mm","index":22},"numberValue":45350.575,"originalNumberValue":45350.575,"type":"NUMBER"},"money":{"data":24,"dataFormatData":{"format":"General","index":0},"formulaData":{"formulaValue":"B2*C2"},"numberValue":24,"originalNumberValue":24,"type":"NUMBER"},"name":{"data":"苹果","dataFormatData":{"format":"General","index":0},"stringValue":"苹果","type":"STRING"},"number":{"data":12,"dataFormatData":{"format":"General","index":0},"numberValue":12,"originalNumberValue":12,"type":"NUMBER"},"price":{"data":2,"dataFormatData":{"format":"General","index":0},"numberValue":2,"originalNumberValue":2,"type":"NUMBER"}
}
读取正常数据:{"date":{"data":1712546400000,"dataFormatData":{"format":"yyyy-m-d h:mm","index":22},"numberValue":45390.4722222222,"originalNumberValue":45390.4722222222,"type":"NUMBER"},"money":{"data":57,"dataFormatData":{"format":"General","index":0},"formulaData":{"formulaValue":"B3*C3"},"numberValue":57,"originalNumberValue":57,"type":"NUMBER"},"name":{"data":"西瓜","dataFormatData":{"format":"General","index":0},"stringValue":"西瓜","type":"STRING"},"number":{"data":19,"dataFormatData":{"format":"General","index":0},"numberValue":19,"originalNumberValue":19,"type":"NUMBER"},"price":{"data":3,"dataFormatData":{"format":"General","index":0},"numberValue":3,"originalNumberValue":3,"type":"NUMBER"}
}
读取正常数据:{"date":{"data":1714533600000,"dataFormatData":{"format":"yyyy-m-d h:mm","index":22},"numberValue":45413.4722222222,"originalNumberValue":45413.4722222222,"type":"NUMBER"},"money":{"data":84,"dataFormatData":{"format":"General","index":0},"formulaData":{"formulaValue":"B4*C4"},"numberValue":84,"originalNumberValue":84,"type":"NUMBER"},"name":{"data":"土豆","dataFormatData":{"format":"General","index":0},"stringValue":"土豆","type":"STRING"},"number":{"data":21,"dataFormatData":{"format":"General","index":0},"numberValue":21,"originalNumberValue":21,"type":"NUMBER"},"price":{"data":4,"dataFormatData":{"format":"General","index":0},"numberValue":4,"originalNumberValue":4,"type":"NUMBER"}
}

2.10 数据转换等异常处理

test9.xlsx:

在这里插入图片描述

数据转换等异常处理,在监听器中重写onException方法即可:

ErrorListener,在转换异常 获取其他异常下会调用本接口,抛出异常则停止读取,如果这里不抛出异常则继续读取下一行:

java">package com.xiaoxu.excel.exp;import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.exception.ExcelDataConvertException;
import com.alibaba.excel.read.listener.ReadListener;
import com.alibaba.fastjson.JSON;/*** @author xiaoxu* @date 2024-05-01 11:07* learn_java:com.xiaoxu.excel.exp.ErrorListener*/
public class ErrorListener implements ReadListener<ErrorObj> {@Overridepublic void onException(Exception exception, AnalysisContext context) throws Exception {System.out.println("解析失败,继续下一行数据解析:"+ exception.getMessage());if (exception instanceof ExcelDataConvertException) {ExcelDataConvertException exp= (ExcelDataConvertException) exception;System.out.println(String.format("格式转换异常," +"第%s行,第%s列解析异常,数据为:%s.",exp.getRowIndex(), exp.getColumnIndex(),exp.getCellData().getStringValue()));}}@Overridepublic void invoke(ErrorObj errorObj, AnalysisContext analysisContext) {System.out.println("读取数据:" +JSON.toJSONString(errorObj, true));}@Overridepublic void doAfterAllAnalysed(AnalysisContext analysisContext) {}
}

ErrorObj:

java">@Setter
@Getter
public class ErrorObj {@ExcelProperty("数目")private long count;
}

TestErrorObj:

java">String file = DeskTool.getPath("test9");
EasyExcel.read(file).head(ErrorObj.class).registerReadListener(new ErrorListener()).sheet().headRowNumber(1).doRead();

执行结果:

java">解析失败,继续下一行数据解析:
Convert data com.alibaba.excel.metadata.data.ReadCellData@dc8f2848 to long error 
格式转换异常,1,0列解析异常,数据为:苹果.
读取数据:{"count":120
}

2.11 不创建对象的读

不创建对象的场景下读取数据,可以使用Map<Integer, String>来存储读取的数据:

java">@Slf4j
public class NoModelDataListener extends AnalysisEventListener<Map<Integer, String>> {/*** 每隔5条存储数据库,实际使用中可以100条,然后清理list ,方便内存回收*/private static final int BATCH_COUNT = 5;private List<Map<Integer, String>> cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);@Overridepublic void invoke(Map<Integer, String> data, AnalysisContext context) {log.info("解析到一条数据:{}", JSON.toJSONString(data));cachedDataList.add(data);if (cachedDataList.size() >= BATCH_COUNT) {saveData();cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);}}@Overridepublic void doAfterAllAnalysed(AnalysisContext context) {saveData();log.info("所有数据解析完成!");}/*** 加上存储数据库*/private void saveData() {log.info("{}条数据,开始存储数据库!", cachedDataList.size());log.info("存储数据库成功!");}
}

不创建对象读取:

java">/*** 不创建对象的读*/
@Test
public void noModelRead() {String fileName = TestFileUtil.getPath() + "demo" + File.separator + "demo.xlsx";// 这里 只要,然后读取第一个sheet 同步读取会自动finishEasyExcel.read(fileName, new NoModelDataListener()).sheet().doRead();
}

2.12 web中的读

参考EasyExcel的读取文件方法,可以传入InputStream进行读取,那么可以结合Spring的web读取excel数据:

java">public static ExcelReaderBuilder read(InputStream inputStream, Class head, ReadListener readListener) {ExcelReaderBuilder excelReaderBuilder = new ExcelReaderBuilder();excelReaderBuilder.file(inputStream);if (head != null) {excelReaderBuilder.head(head);}if (readListener != null) {excelReaderBuilder.registerReadListener(readListener);}return excelReaderBuilder;
}

web读取如下:

java">/*** 文件上传* <p>* 1. 创建excel对应的实体对象 参照{@link UploadData}* <p>* 2. 由于默认一行行的读取excel,所以需要创建excel一行一行的回调监听器,参照{@link UploadDataListener}* <p>* 3. 直接读即可*/
@PostMapping("upload")
@ResponseBody
public String upload(MultipartFile file) throws IOException {EasyExcel.read(file.getInputStream(), UploadData.class, new UploadDataListener(uploadDAO)).sheet().doRead();return "success";
}

http://www.ppmy.cn/ops/27714.html

相关文章

Windows下载MingGW

因为要配置vscode的c/c环境&#xff0c;需要下载一个编译器&#xff0c;gcc官方推荐开源的MingGW-W64&#xff0c;看了几个下载方法&#xff0c;决定用最简单的离线安装。 niXman/mingw-builds-binaries/releases 32位的操作系统&#xff1a;i686&#xff0c;64位的操作系统&a…

《Redis使用手册之Lua脚本》

《Redis使用手册之Lua脚本》 EVAL&#xff1a;执行脚本 127.0.0.1:6379> eval “return ‘hello world’” 0 “hello world” 127.0.0.1:6379> eval “return redis.call(‘set’,KEYS[1],ARGV[1])” 1 “message” “hello world” OK 127.0.0.1:6379> get message…

使用FPGA实现串-并型乘法器

介绍 其实我们知道&#xff0c;用FPGA实现乘法器并不是一件很简单的事&#xff0c;而且在FPGA中也有乘法器的IP核可以直接调用&#xff0c;我这里完全就是为了熟悉一些FPGA的语法然后写了这样一个电路。 串-并型乘法器模块 从字面上看&#xff0c;串-并乘法器就是其中一个乘数…

岚图汽车与东软睿驰签署战略合作协议

4月26日,东软睿驰与岚图汽车正式签署战略合作协议,双方将结合在各自领域拥有的产业资源、技术研发和资本运作等优势,聚焦智能化产品和应用,建立长期共赢的战略合作伙伴关系,通过不断探索未来新技术、新产业、新业态和新模式,围绕用户需求共同打造极致的智能出行体验。 图为岚图…

小旋风蜘蛛池优化版

下载地址&#xff1a;小旋风蜘蛛池.zip 配置的时候需要做伪静态 #如果用的是宝塔面板设置伪静态&#xff0c;则去掉 第一行 的 location / { 和最后一行的 } location / { rewrite ^/template/(.*)\.html$ /index.php last; rewrite ^/temp/(data|db|robotlog|tplrules|errp…

OpenHarmony实战开发-动画曲线、如何实现动画衔接

UI界面除了运行动画之外&#xff0c;还承载着与用户进行实时交互的功能。当用户行为根据意图变化发生改变时&#xff0c;UI界面应做到即时响应。例如用户在应用启动过程中&#xff0c;上滑退出&#xff0c;那么启动动画应该立即过渡到退出动画&#xff0c;而不应该等启动动画完…

解锁图像新维度:剑桥联手英特尔,利用大语言模型重构逆向图形学!

DeepVisionary 每日深度学习前沿科技推送&顶会论文分享&#xff0c;与你一起了解前沿深度学习信息&#xff01; 引言&#xff1a;探索逆图形学的新视角 逆图形学&#xff08;Inverse Graphics&#xff09;是计算机视觉和图形学中的一个基本挑战&#xff0c;它涉及将图像…

Python项目开发实战:如何基于Keras的深度学习来预测国际旅行人数

注意:本文的下载教程,与以下文章的思路有相同点,也有不同点,最终目标只是让读者从多维度去熟练掌握本知识点。 下载教程:深度学习-基于Keras的Python项目开发实战_国际旅行人数预测_编程案例实例教程.pdf 在预测国际旅行人数这一问题上,我们可以利用深度学习技术,尤其是…