读取文件导入的话,我们经常看到下面这些方法。
//同步的返回,不推荐使用
EasyExcel.read(file).sheet(sheetNo).head(Class.class).headRowNumber(headRowNum)..doReadSync();
//异步的,通过监听器处理读到的数据。
EasyExcel.read(file).sheet(sheetNo).head(Class.class).headRowNumber(headRowNum).registerReadListener(监听器)doRead();
首先建议大家可以看一下 EasyExcel为我们提供的 EasyExcelFactory工厂类相关的源代码,看懂源代码的话,对我们自己编写适合自己项目的方法很用用处。
引入依赖:
<dependency>
<groupId>com.alibabagroupId>
<artifactId>easyexcelartifactId>
<version>3.1.1version>
dependency>
方法:EasyExcel.read(file)
查看 read()方法:
EasyExcel类调用 read方法,实际调用的是 EasyExcelFactory类的方法。
查看 EasyExcelFactory类方法:
EasyExcelFactory工厂类定义了许多读和写的重载方法。主要看读方法,分为两类:
ExcelWriterBuilder类
。ExcelReaderSheetBuilder类
。从这里我们可以看出,EasyExcelFactory工厂类创建了 XxxBuilder类,并返回了 XxxBuilder类,那是不是我们也可以直接使用 XxxBuilder类操作读文件。答案是肯定的。
查看 ExcelReaderBuilder类:
ExcelReaderBuilder类实例化时,创建了 ReadWorkbook对象。
查看 ExcelReaderBuilder类方法:
ExcelReaderBuilder类主要处理 Excel文件和相关文件属性信息,比如设置 字符编码、文件加密的密码,忽略处理哪行数据等。
查看 ExcelReaderSheetBuilder类:
ExcelReaderSheetBuilder类实例化时,创建了 ReadSheet对象和 ExcelReader对象。
查看 ExcelReaderSheetBuilder类方法:
ExcelReaderSheetBuilder类主要处理 Excel文件中的每一个 sheet信息,比如设置要读取的sheet名,索引。
调用 doRead()方法其实底层通过 ExcelReader对象读取每一个 sheet信息。
查看 ExcelReader类:
查看 ExcelReader类方法:
可以看出 ExcelReader类包含了 ReadWorkbook对象和 ReadSheet对象。从而处理 Excel文件中的每一个 sheet信息。
思考:
(1)ExcelReader类是如何初始化的?
在 ExcelReaderBuilder类调用 sheet()方法时,初始化了 ExcelReader对象。
在 ExcelReaderSheetBuilder类调用 doRead()方法时,底层就通过 ExcelReader对象遍历读取每一个 sheet信息。
(2)ReadWorkbook对象和 ReadSheet对象是如何赋值给 ExcelReader类的?
在各自的XxxBuilder类中通过构造方法初始化时,创建的,然后分别在调用 sheet()方法和 doRead()方法各自的 build()方法中完成赋值的。
有了上面的知识,接下来通过 EasyExcel操作读 Excel文件就比较简单了。
一般情况,我们创建一个对象来和 Excel文件sheet的列名建立映射关系。
指定列的下标或者列名:
@ExcelProperty注解
,不建议 index 和 name 同时用,要么一个对象只用index,要么一个对象只用name去匹配。自定义格式转换:
下面我们创建一个Excel文件。
根据 Sheet信息创建一个映射类:
@Getter
@Setter
@EqualsAndHashCode
public class DemoData {
@ExcelProperty("标题")
private String string;
@ExcelProperty("日期")
private Date date;
@ExcelProperty("浮点数据")
private Double doubleData;
}
同步读并返回数据,不推荐使用。
public static void main(String[] args) {
syncRead();
}
private static void syncRead() {
String fileName = "D:\\TempFiles\\表格.xlsx";
List<DemoData> demoDataList = EasyExcel.read(fileName)
.sheet()
.head(DemoData.class)
.headRowNumber(1)
.doReadSync(); //同步读
log.info("同步解析到所有数据为:{}", JSON.toJSONString(demoDataList));
}
注册一个自带的匿名监听器。
public static void asyncRead() {
String fileName = "D:\\TempFiles\\表格.xlsx";
EasyExcel.read(fileName)// 读取Excel文件
.sheet(0) // 读取哪个sheet,索引从0开始
.head(DemoData.class) // 设置映射对象
.headRowNumber(1) // 设置1,因为头值占了一行。如果多行头,就设置几行。索引从1开始
.registerReadListener(new AnalysisEventListener<DemoData>() { //注册读的监听器
/**
* 每解析一行excel数据,就会被调用一次
* @param demoData
* @param analysisContext
*/
@Override
public void invoke(DemoData demoData, AnalysisContext analysisContext) {
log.info("解析到一条数据为:{}", JSON.toJSONString(demoData));
}
/**
* 全部解析完被调用
* @param analysisContext
*/
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
log.info("全部解析完成");
}
})
.doRead();
}
监听器可以理解为是对读取的数据进行校验(空校验,类型校验)和处理的逻辑部分,用于异步读取。
在上面异步读中的代码,使用了一个参数就是 AnalysisEventListener
的监听器。
AnalysisEventListener类实现了 ReadListener接口,ReadListener中有下面几个方法:
public interface ReadListener<T> extends Listener {
// 在转换异常获取其他异常下会调用本接口。
default void onException(Exception exception, AnalysisContext context) throws Exception {
throw exception;
}
//读取表头数据存在headMap中
default void invokeHead(Map<Integer, ReadCellData<?>> headMap, AnalysisContext context) {}
//读取一行一行数据到data
void invoke(T data, AnalysisContext context);
void extra(CellExtra var1, AnalysisContext context);
//在完成所有数据解析后进行的操作。AOP思想。
void doAfterAllAnalysed(AnalysisContext context);
default boolean hasNext(AnalysisContext context) {
return true;
}
}
我们可以根据进行自己需求的扩展 AnalysisEventListener这个类或者ReadListener接口,对那几个方法进行扩展。
在上面的Excel文件中我们再创建一个Sheet。
对应再创建一个映射类:
@Getter
@Setter
@EqualsAndHashCode
public class DemoData2 {
@ExcelProperty("标题")
private String string;
@ExcelProperty("日期")
private Date date;
@ExcelProperty("浮点数据")
private Double doubleData;
@ExcelProperty("整数")
private Integer integerData;
/**
* Java String类型会丢失精度,建议定义为 BigDecimal|Double类型
*/
@ExcelProperty("经度")
private Double longitude;
@ExcelProperty("纬度")
private Double latitude;
}
public static void manySheetRead() {
String fileName = "D:\\TempFiles\\表格.xlsx";
ExcelReader excelReader = EasyExcel.read(fileName).build();
ReadSheet readSheet1 = EasyExcel.readSheet(0).head(DemoData.class).headRowNumber(1)
.registerReadListener(new AnalysisEventListener<DemoData>() { //注册读的监听器
/**
* 每解析一行excel数据,就会被调用一次
* @param demoData
* @param analysisContext
*/
@Override
public void invoke(DemoData demoData, AnalysisContext analysisContext) {
log.info("readSheet1 解析到一条数据为:{}", JSON.toJSONString(demoData));
}
/**
* 全部解析完被调用
* @param analysisContext
*/
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
log.info("readSheet1 全部解析完成");
}
}).build();
ReadSheet readSheet2 = EasyExcel.readSheet(1).head(DemoData2.class)
.headRowNumber(2) // 注意Sheet2表头占了两行
.registerReadListener(new AnalysisEventListener<DemoData2>() { //注册读的监听器
/**
* 每解析一行excel数据,就会被调用一次
* @param demoData2
* @param analysisContext
*/
@Override
public void invoke(DemoData2 demoData2, AnalysisContext analysisContext) {
log.info("readSheet2 解析到一条数据为:{}", JSON.toJSONString(demoData2));
}
/**
* 全部解析完被调用
* @param analysisContext
*/
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
log.info("readSheet2 全部解析完成");
}
}).build();
excelReader.read(readSheet1, readSheet2);
}
自定义一个通用的读监听器,继承 AnalysisEventListener类。
@Slf4j
public class CustomEasyExcelReadListener<T> extends AnalysisEventListener<T> {
// 保存读取的对象
private final List<T> rows = new ArrayList<>();
// Sheet对应的名字
private String sheetName = "";
// 获取对应类
private Class headClazz;
// 此集合用来存储错误信息
private final List<String> errorMessage = new ArrayList<>();
public CustomEasyExcelReadListener(Class headClazz) {
this.headClazz = headClazz;
}
/**
* 通过Class获取类字段信息
*
* @param headClazz
* @return
* @throws NoSuchFieldException
*/
public Map<Integer, String> getIndexNameMap(Class headClazz) throws NoSuchFieldException {
Map<Integer, String> result = new HashMap<>();
Field field;
Field[] fields = headClazz.getDeclaredFields(); //获取类中所有的属性
for (int i = 0; i < fields.length; i++) {
field = headClazz.getDeclaredField(fields[i].getName());
//log.info(String.valueOf(field));
field.setAccessible(true);
ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class);//获取根据注解的方式获取ExcelProperty修饰的字段
if (excelProperty != null) {
int index = excelProperty.index(); //索引值
String[] values = excelProperty.value(); //字段值
StringBuilder value = new StringBuilder();
for (String v : values) {
value.append(v);
}
result.put(index, value.toString());
}
}
return result;
}
/**
* 读取表头数据存在headMap中。如果你校验表头格式时可以使用。
*
* @param headMap
* @param context
*/
@Override
public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
log.info("解析到一条表头数据:{}", JSON.toJSONString(headMap));
Map<Integer, String> head = new HashMap<>();
try {
//通过Class获取到使用@ExcelProperty注解配置的字段
head = getIndexNameMap(headClazz);
log.info(String.valueOf(head));
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
//解析到的excel表头和实体配置的进行比对
Set<Integer> keySet = head.keySet();
for (Integer key : keySet) {
if (StringUtils.isEmpty(headMap.get(key))) {
errorMessage.add("您上传的文件第" + (key + 1) + "列表头为空,请按照模板检查后重新上传");
}
if (!headMap.get(key).equals(head.get(key))) {
errorMessage.add("您上传的文件第" + (key + 1) + "列表头与模板表头不一致,请检查后重新上传");
}
}
}
/**
* 读取一行一行数据到object
*
* @param object
* @param context
*/
@Override
public void invoke(T object, AnalysisContext context) {
// 实际数据量比较大时,rows里的数据可以存到一定量之后进行批量处理(比如存到数据库),
// 然后清空列表,以防止内存占用过多造成OOM
rows.add(object);
}
/**
* 在完成数据解析后进行的操作。AOP思想。
*
* @param context
*/
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
// 当前sheet的名称 编码获取类似
sheetName = context.readSheetHolder().getSheetName();
log.info("sheetName = {} -> 所有数据解析完成, read {} rows", sheetName, rows.size());
}
/**
* 在转换异常 获取其他异常下会调用本接口。抛出异常则停止读取。如果这里不抛出异常则 继续读取下一行。
*
* @param exception 抛出异常
* @param context 解析内容
*/
@Override
public void onException(Exception exception, AnalysisContext context) {
log.error("解析失败,但是继续解析下一行:{}", exception.getMessage());
if (exception instanceof ExcelDataConvertException) {
ExcelDataConvertException excelDataConvertException = (ExcelDataConvertException) exception;
errorMessage.add("第" + excelDataConvertException.getRowIndex() + "行,第" + (excelDataConvertException.getColumnIndex() + 1) +
"列数据类型解析异常,数据为:" + excelDataConvertException.getCellData());
log.error("第{}行,第{}列数据类型解析异常,数据为:{}", excelDataConvertException.getRowIndex(), excelDataConvertException.getColumnIndex() + 1,
excelDataConvertException.getCellData());
}
}
public List<T> getRows() {
return rows;
}
public Class getHeadClazz() {
return headClazz;
}
public List<String> getErrorMessage() {
return errorMessage;
}
public String getSheetName() {
return sheetName;
}
}
/**
* 自定义 ReadListener监听器测试方法
*/
private static void manySheetReadWthCustomReadListener() {
String fileName = "D:\\TempFiles\\表格.xlsx";
ExcelReader excelReader = EasyExcel.read(fileName).build();
CustomEasyExcelReadListener mySheet1Listener = new CustomEasyExcelReadListener(DemoData.class);
CustomEasyExcelReadListener mySheet2Listener = new CustomEasyExcelReadListener(DemoData2.class);
List<ReadSheet> readSheetList = new ArrayList<>();
ReadSheet readSheet1 = EasyExcel.readSheet(0).head(DemoData.class).headRowNumber(1)
.registerReadListener(mySheet1Listener).build();
ReadSheet readSheet2 = EasyExcel.readSheet(1).head(DemoData2.class)
.headRowNumber(2) // 注意Sheet2表头占了两行
.registerReadListener(mySheet2Listener).build();
readSheetList.add(readSheet1);
readSheetList.add(readSheet2);
excelReader.read(readSheetList);
System.out.println("============Sheet1 解析解析完成,数据如下================");
//获取Sheet1监听器读到的数据,拿到的数据大家可以根据需求进行数据库操作
List rows = mySheet1Listener.getRows();
for (Object row : rows) {
log.info("Sheet1 解析到一条数据为:{}", JSON.toJSONString(row));
}
//获取解决出的错误信息
List<String> errorMessage = mySheet1Listener.getErrorMessage();
log.info("Sheet1 解析错误信息为:{}", JSON.toJSONString(errorMessage));
System.out.println("============Sheet2 解析解析完成,数据如下================");
//获取Sheet2监听器读到的数据,拿到的数据大家可以根据需求进行数据库操作
List<DemoData2> demoData2List = mySheet2Listener.getRows();
for (DemoData2 demoData2 : demoData2List) {
log.info("Sheet2 解析到一条数据为:{}", JSON.toJSONString(demoData2));
}
//获取解决出的错误信息
List<String> errorMessage2 = mySheet2Listener.getErrorMessage();
log.info("Sheet2 解析错误信息为:{}", JSON.toJSONString(errorMessage2));
}
通用的自定义读监听器中 invokeHeadMap方法校验有点不合适,大家可以创建针对性的读监听器,分别处理。一般 invokeHeadMap方法根据需要使用。
更多操作查看官方文档:https://easyexcel.opensource.alibaba.com/
– 求知若饥,虚心若愚。