以下代码是一个Excel数据监听器,用于监听和处理Excel数据的读取事件。它实现了AnalysisEventListener接口,并重写了其中的方法。以下是代码中的主要部分:
在使用EasyExcel读取Excel文件时,如果没有指定表头所在的行数,EasyExcel会默认根据内容进行智能识别,尝试找到表头所在的行
。
这段代码的作用是根据传入的Excel模板路径和文件名,解析Excel文件,并进行表头校验和数据行处理,文章中只处理了按照模板校验表头是否合法,针对动态传入List
形式的表头可自行扩展
<dependency>
<groupId>com.alibabagroupId>
<artifactId>easyexcelartifactId>
<version>2.2.6version>
dependency>
<dependency>
<groupId>org.dromara.hutoolgroupId>
<artifactId>hutool-allartifactId>
<version>6.0.0-M10version>
dependency>
【注意】:这里推荐EasyExcel升级到3.3.1以上版本
,2.X已经不维护了
/**
* Excel数据监听器,用户监听和处理Excel数据的读取事件
* 实现了EasyExcel库的AnalysisEventListener接口,实现了相应的回掉方法
*
* @author 管理员
* @date 2024/2/27
* @param 要解析的Excel数据对象的类型
*/
@Slf4j
public class ExcelListener < T > extends AnalysisEventListener < T > {
/**
* 文件特殊字符规则
*/
private static final String FILE_SPECIAL_REGEX = "[\\/:*?\"<>|;.]";
/**
* 用于存储解析过程中数据行信息
*/
private final List < T > dataList = Lists.newArrayList();
/**
* Excel模板路径(此实现是resource路径)
*/
private String templatePath;
/**
* 模板表头集合
*/
private List < String > templateHeaderList;
/**
* Excel数据监听器构造方法
*
* @author 管理员
* @date 2024/3/4
*/
public ExcelListener() {
}
/**
* Excel数据监听器构造方法,此构造方法会校验表头是否合法【注意】:此方法默认为第一行为表头,如不是请重写构造方法传入表头对应行数
*
* @author 管理员
* @date 2024/3/4
* @param templatePath 模板路径(resource路径)
*/
public ExcelListener(String templatePath) {
this.templatePath = templatePath;
this.templateHeaderList = readTemplateHeader();
}
/**
* Excel数据监听器构造方法,此构造方法会校验表头是否合法,文件名称非法
* 【注意】:此方法默认为第一行为表头,如不是请重写构造方法传入表头对应行数
*
* @author 管理员
* @date 2024/3/4
* @param templatePath 模板路径(resource路径)
* @param excelFileName 上传Excel文件名称
* @throws BizException 业务异常
*/
public ExcelListener(String templatePath, String excelFileName) {
// 文件名不能包含特殊字符
if (ReUtil.contains(FILE_SPECIAL_REGEX, FileNameUtil.mainName(excelFileName))) {
throw new BizException("业务错误码","误码提示");
}
// 判断文件后缀是否是Excel 文件格式错误
if (!ReUtil.isMatch("(xlsx|xls)$", FileNameUtil.extName(excelFileName).toLowerCase())) {
throw new new BizException("业务错误码","误码提示");
}
this.templatePath = templatePath;
this.templateHeaderList = readTemplateHeader();
}
@Override
public void invokeHead(Map < Integer, CellData > headMap, AnalysisContext context) {
// 当解析Excel表格的表头时触发的回调方法,此处可在类中添加成员变量,校验表头合法性
// invokeHead抛出异常只会终止当前invokeHead方法的执行,可在OnException中处理并终止程序
// headMap存储所有表头信息,可与成员变量对比,不一致则不合法
// 未传入模板路劲则表示无须对比模板表头,直接调用默认父类方法
if (CharSequenceUtil.isBlank(templatePath)) {
super.invokeHead(headMap, context);
return;
}
// 模板表头无内容表示模板错误
if (CollUtil.isEmpty(templateHeaderList)) {
throw new BizException("业务错误码","误码提示");
}
List < String > currentHeaderList = Lists.newArrayListWithCapacity(headMap.size());
headMap.forEach((columnIndex, cellData) -> currentHeaderList.add(cellData.getStringValue()));
// 比较模板的表头与当前上传文件的表头,如果两个表头不一致则表示模板错误,抛出异常
if (!CollUtil.isEqualList(templateHeaderList, currentHeaderList)) {
throw new BizException("业务错误码","误码提示");
}
// 无任何异常执行直接调用默认父类方法
super.invokeHead(headMap, context);
}
@Override
public void invoke(T rowData, AnalysisContext analysisContext) {
// 当解析Excel表格的数据行时触发的回调方法,在这个方法中可以处理每一行的数据逻辑
dataList.add(rowData);
}
@Override
public void onException(Exception exception, AnalysisContext context) {
// onException抛出异常会终止解析,并在最外层可捕获该异常
// 解析过程中发生异常的逻辑,一般记录日志
if (exception instanceof ExcelDataConvertException) {
ExcelDataConvertException excelDataConvertException = (ExcelDataConvertException) exception;
StringBuilder stringBuffer = new StringBuilder();
stringBuffer.append("第").append(excelDataConvertException.getRowIndex()).append("行,")
.append(excelDataConvertException.getColumnIndex()).append("列解析异常").append("异常数据为:")
.append(excelDataConvertException.getCellData()).append("异常列属性为:")
.append(excelDataConvertException.getExcelContentProperty().getHead());
log.error(stringBuffer.toString());
throw new BizException("业务错误码",stringBuffer.toString());
}
else if (exception instanceof BizException) {
throw (BizException) exception;
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
// 当整个Excel表格解析完成时触发的回调方法,解析完毕之后一般有以下逻辑处理
// 数据处理、数据存储、数据校验、业务通知
// 逻辑1:当dataList为空,标识文件中没有数据行,可以认为是一个人空模板
if (CollUtil.isEmpty(dataList)) {
throw new BizException("业务错误码","误码提示");
}
}
/**
* 获取解析过程中的数据行信息
*
* @author 管理员
* @date 2024/2/27
* @return 数据行信息
*/
public List < T > getDataList() {
return dataList;
}
/**
* 读取Excel模板表头
*
* @author 管理员
* @date 2024/3/4
* @return Excel模板表头
* @throws BizException 业务异常
*/
private List < String > readTemplateHeader() {
// 无模板路径不解析模板表头
if (CharSequenceUtil.isBlank(templatePath)) {
return Lists.newArrayList();
}
try (InputStream inputStream = ResourceUtil.getStreamSafe(templatePath)) {
List < Map < Integer, String > > headList = EasyExcelFactory.read(inputStream).headRowNumber(0).sheet()
.doReadSync();
// 读取表头不为空,只要模板没问题,肯定不会空
if (CollUtil.isNotEmpty(headList)) {
Map < Integer, String > headMap = headList.get(0);
List < String > headerList = Lists.newArrayListWithExpectedSize(headMap.size());
headMap.forEach((key, value) -> headerList.add(value));
return headerList;
}
}
catch (IOException ex) {
log.error("读取Excel模板表头异常", ex);
throw new BizException("业务错误码","误码提示");
}
return Lists.newArrayList();
}
}
【说明】:下面代码是使用EasyExcel版本为:3.3.1,自定义转换器支持LocalDateTime,试用了官方的LocalDateTimeDateConverter
发现效果并不理想,所以自行编写了一个。
/**
* 阿里EasyExcel时间转换器
*
* @author 虾酱
* @since 2024/3/12 17:15
*/
public class LocalDateTimeConverter implements Converter < LocalDateTime > {
/**
* Excel时间转换格式
*/
private final String dateFormat;
public LocalDateTimeConverter(String dateFormat) {
this.dateFormat = dateFormat;
}
@Override
public Class < LocalDateTime > supportJavaTypeKey() {
return LocalDateTime.class;
}
@Override
public CellDataTypeEnum supportExcelTypeKey() {
return CellDataTypeEnum.STRING;
}
@Override
public LocalDateTime convertToJavaData(ReadCellData < ? > cellData, ExcelContentProperty contentProperty,
GlobalConfiguration globalConfiguration) {
// 应用场景:导入
return TimeUtil.parse(cellData.getStringValue(), dateFormat);
}
@Override
public WriteCellData < String > convertToExcelData(LocalDateTime localDateTime,
ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
// 应用场景:导出
return new WriteCellData <>(TimeUtil.format(localDateTime, dateFormat));
}
}
方式一:表头文件什么的都不做任何校验
ExcelListener < T > excelListener = new ExcelListener <>();
方式二:此种方式只校验表头
ExcelListener < T > excelListener = new ExcelListener <>(templatePath);
方式三:最后这种方式校验表头与文件的合法性
ExcelListener < T > excelListener = new ExcelListener <>(templatePath,fileName);
excelListener 传入EasyExcelFactory中实现文件解析并转换为对应实体对象
EasyExcelFactory.read(inputStream, clazz, excelListener).sheet().doRead();
总而言之,该Excel数据监听器可以帮助用户高效、准确地解析和处理Excel数据。它提供了表头校验、数据处理和异常处理等功能,使得数据的导入和处理变得更加简单和可靠。无论是处理大规模数据还是进行数据校验,该监听器都能够帮助用户提升工作效率,减少错误率。
【扩展内容】:保留每一行数据的行号
这种情况适用于需要在处理Excel数据时保留每一行数据的行号的场景。例如:
数据校验:在处理Excel数据时,你可能需要对每一行数据进行校验。如果校验失败,你可能希望知道是哪一行数据出错了。通过在ExcelRow对象中保存行号,你可以方便地定位到出错的行。
数据处理:在处理Excel数据时,你可能需要根据每一行的行号进行特定的数据处理操作。例如,你可能需要根据行号将数据存储到不同的数据库表中,或者根据行号做一些特定的逻辑处理。
错误处理:如果在处理Excel数据时发生错误,你可能希望进行相应的错误处理,并记录下出错的行号。通过ExcelRow对象,你可以方便地获取到出错行的行号,并进行错误处理。
总之,通过在ExcelRow对象中添加行号字段,你可以在处理Excel数据时方便地获取到每一行数据及其对应的行号,这样能够更加灵活地进行后续的数据处理和错误处理操作。
在T对象中添加一个行数字段,可以通过创建一个新的包含行数字段的类来实现。这个新的类可以包含两个字段:rowData表示Excel的每一行数据,rowNumber表示行数。
public class ExcelRow<T> {
private T rowData; // Excel每一行的数据
private int rowNumber; // 行数
public ExcelRow(T rowData, int rowNumber) {
this.rowData = rowData;
this.rowNumber = rowNumber;
}
public T getRowData() {
return rowData;
}
public int getRowNumber() {
return rowNumber;
}
}
public class ExcelListener<T> extends AnalysisEventListener<T> {
private List<ExcelRow<T>> dataList = new ArrayList<>();
// ...
@Override
public void invoke(T rowData, AnalysisContext analysisContext) {
int rowNumber = analysisContext.readRowHolder().getRowIndex();
ExcelRow<T> excelRow = new ExcelRow<>(rowData, rowNumber);
dataList.add(excelRow);
}
// ...
public List<ExcelRow<T>> getDataList() {
return dataList;
}
}
在上述代码中,invoke方法通过analysisContext.readRowHolder().getRowIndex()获取到当前行的行数,然后创建ExcelRow对象并添加到dataList中。
通过使用ExcelRow对象,你可以获取到每一行数据及其对应的行数。