基于EasyExcel实现导入数据校验,生成错误信息Excel

有答案直接发给我 2024-09-06 17:35:01 阅读 81

目录

功能设计设计思路代码解析捕获类型转换异常在AnalysisEventListener中实现校验逻辑获取到Error后,根据错误信息生成Excel

功能设计

<code>由于项目中涉及到大量的文件导入功能,故考虑设计一个excel导入的通用框架,解决以下问题

导入的数据不可信任,可能出现空值校验的许多判断,如果将这些判断加入业务代码可能会造成大量代码的堆积,如下情况:

if(name==null){ -- -->

throw new RuntimeException("名称不能为空");

}

if(age==null){

throw new RuntimeException("年龄不能为空");

}

if(sex==null){

throw new RuntimeException("性别不能为空");

}

if(order.size()>10){

throw new RuntimeException("订单号长度不能大于10");

}

EasyExcel帮我处理导入文件时,只是简单的根据列名把内容set到字段上,如果字段类型不符是会直接报错的!而我们需要将数据的错误内容提交给用户,所以如下的报错是不可取的

在这里插入图片描述

针对文件中的问题,需要清晰地呈现给用户,每一行具体出现了哪种类型的错误,例如如下:

在这里插入图片描述

基于EasyExcel封装,由于项目中本身使用的EasyExcel,考虑到不改动项目的技术组成,还是基于EasyExcel开发。

设计思路

EasyExcel做的工作其实很简单,就是把文件中的内容映射到我们实体类的字段上,我们要做的就是在映射前和映射后做校验

在这里插入图片描述

代码解析

我先把完整代码贴上,下面再详细分析

注解类

<code>/**

* 导入校验注解

*

* @author wangmeng

* @since 2024/5/25

*/

@Retention(RetentionPolicy.RUNTIME)

@Target(ElementType.FIELD)

public @interface ExcelCheck { -- -->

/**

* 是否可以为空,默认是

*/

boolean canEmpty() default true;

/**

* 是否可以重复,默认是

*/

boolean canRepeat() default true;

/**

* 长度校验,只对String生效

*/

int length() default -1;

}

错误信息实体类

/**

* excel导入错误信息

*

* @author wangmeng

* @since 2024/5/25

*/

@Data

@AllArgsConstructor

@NoArgsConstructor

@Accessors(chain = true)

public class ExcelErrorMessage {

/**

* 行号

*/

private Integer rowNum;

/**

* 列名

*/

private String colHeaderName;

/**

* 错误信息

*/

private String message;

}

导入通用的listener

/**

* excel导入共通监听类

*

* @author wangmeng

* @since 2024/5/25

*/

@Slf4j

public class CheckableImportListener<T> extends AnalysisEventListener<T> {

/**

* check注解对象

*/

protected List<Object[]> filedList;

/**

* excel数据

*/

protected final List<T> list = new ArrayList<>();

/**

* 错误信息集合

*/

@Getter

private final List<ExcelErrorMessage> errorList = new ArrayList<>();

private Boolean isEmpty = false;

public CheckableImportListener() {

super();

}

@Override

public void onException(Exception exception, AnalysisContext context) throws Exception {

log.error("解析单元格失败,", exception);

if (exception instanceof ExcelDataConvertException) {

ExcelDataConvertException excelDataConvertException = (ExcelDataConvertException) exception;

log.error("第{}行,第{}列解析异常,数据为:{}", excelDataConvertException.getRowIndex(),

excelDataConvertException.getColumnIndex(), excelDataConvertException.getCellData());

}

}

@Override

public void invoke(T data, AnalysisContext context) {

if (CollectionUtils.isEmpty(list)) {

Class<?> clazz = data.getClass();

//含check注解的字段

filedList = Arrays.stream(clazz.getDeclaredFields())

.filter(o -> null != o.getAnnotation(ExcelCheck.class))

.map(o -> new Object[]{ o, o.getAnnotation(ExcelCheck.class), o.getAnnotation(ExcelProperty.class)}).collect(Collectors.toList());

}

log.info("data:{}", JSON.toJSONString(data));

list.add(data);

if (CollectionUtils.isNotEmpty(filedList)) {

checkEmpty(data);

//存在空值则不进行其他校验

if (isEmpty) {

return;

}

// 校验长度

checkLength(data);

}

}

@Override

public void doAfterAllAnalysed(AnalysisContext context) {

if (isEmpty) {

return;

}

errorList.sort(Comparator.comparing(ExcelErrorMessage::getRowNum));

}

/**

* 检验非空

*

* @param data

*/

public void checkEmpty(T data) {

for (Object[] os : filedList) {

Field filed = (Field) os[0];

filed.setAccessible(true);

ExcelCheck excelCheck = (ExcelCheck) os[1];

ExcelProperty excelProperty = (ExcelProperty) os[2];

try {

//校验非空

if (!excelCheck.canEmpty()) {

if (filed.get(data) == null ||

(filed.getType() == String.class && StringUtils.isEmpty((String) filed.get(data)))) {

errorList.add(new ExcelErrorMessage()

.setRowNum(list.size() + 1)

.setColHeaderName(excelProperty.value()[0])

.setMessage(excelProperty.value()[0] + "字段不能为空!"));

isEmpty = true;

}

}

} catch (IllegalAccessException e) {

log.error("校验excel信息失败,", e);

e.printStackTrace();

}

}

}

/**

* 校验长度

*

* @param data

*/

public void checkLength(T data) {

for (Object[] os : filedList) {

Field filed = (Field) os[0];

filed.setAccessible(true);

ExcelCheck excelCheck = (ExcelCheck) os[1];

ExcelProperty excelProperty = (ExcelProperty) os[2];

try {

//校验非空

if (excelCheck.length() > 0 && filed.getType() == String.class) {

String value = (String) filed.get(data);

if (value.length() > excelCheck.length()) {

errorList.add(new ExcelErrorMessage()

.setRowNum(list.size() + 1)

.setColHeaderName(excelProperty.value()[0])

.setMessage(excelProperty.value()[0] + "字段长度大于" + excelCheck.length() + "!"));

}

}

} catch (IllegalAccessException e) {

log.error("校验字段长度失败,", e);

throw new RuntimeException(e);

}

}

}

/**

* 检验重复

*/

public void checkRepeat() {

List<Object[]> repeatAnnotation = filedList.stream().filter(o -> {

ExcelCheck excelCheck = (ExcelCheck) o[1];

return !excelCheck.canRepeat();

}).collect(Collectors.toList());

for (Object[] objects : repeatAnnotation) {

ExcelProperty property = (ExcelProperty) objects[2];

//使用iterate方式构建流以获取行号

Stream.iterate(0, i -> i + 1).limit(list.size()).collect(Collectors.groupingBy(i -> {

Field field = (Field) objects[0];

String result = "";

try {

field.setAccessible(true);

result = JSON.toJSONString(field.get(list.get(i)));

} catch (IllegalAccessException e) {

e.printStackTrace();

}

return result;

}, LinkedHashMap::new, Collectors.mapping(i -> i + 2, Collectors.toList())))

.forEach((k, v) -> {

if (v.size() > 1) {

for (int i = 0; i < v.size(); i++) {

if (i == 0) {

continue;

}

errorList.add(new ExcelErrorMessage()

.setRowNum(v.get(i))

.setColHeaderName(property.value()[0])

.setMessage(property.value()[0] + "字段重复!"));

}

}

});

}

}

public void addError(Integer index, String errorMessage) {

ExcelErrorMessage excelErrorMessage = new ExcelErrorMessage().setRowNum(index).setMessage(errorMessage);

errorList.add(excelErrorMessage);

}

}

导入处理器类

/**

* excel导入处理器,在easyExcel基础封装,增加通用读取、校验功能

*

* @author wangmeng

* @since 2024/6/7

*/

@Setter

@Getter

@Accessors(chain = true)

@Slf4j

public class ExcelImportProcessor {

/**

* 默认校验类型listener

*/

private CheckableImportListener<?> listener = new CheckableImportListener<>();

private Consumer<ExcelReaderBuilder> readerBuilderConsumer;

/**

* 默认第一个sheet

*/

private Integer sheetNo = 0;

/**

* 错误列名

*/

private final static String ERROR_COLUMN_NAME = "错误信息";

public ExcelImportProcessor() {

}

public ExcelImportProcessor(CheckableImportListener<?> listener) {

this.listener = listener;

}

public <R> List<R> importData(MultipartFile file, Class<R> clazz) {

// 校验文件

validateExcel(file);

List<R> dataList = null;

try (InputStream inputStream = file.getInputStream()) {

ExcelReaderBuilder readerBuilder = EasyExcel.read(inputStream, clazz, listener);

if (readerBuilderConsumer != null) {

readerBuilderConsumer.accept(readerBuilder);

}

dataList = readerBuilder.sheet(sheetNo).doReadSync();

} catch (ExcelAnalysisException e) {

ExcelDataConvertException exception = (ExcelDataConvertException) e.getCause();

List<ExcelErrorMessage> errorList = listener.getErrorList();

String headerName = exception.getExcelContentProperty().getField().getAnnotation(ExcelProperty.class).value()[0];

errorList.add(new ExcelErrorMessage().setRowNum(exception.getRowIndex() + 1)

.setColHeaderName(headerName)

.setMessage("'" + headerName + "'类型转换失败,请输入正确格式"));

} catch (IOException ioe) {

log.info("导入失败,异常,", ioe);

throw new RuntimeException("导入失败!");

}

if (CollectionUtils.isEmpty(dataList)) {

throw new RuntimeException("解析数据为空!");

}

return dataList;

}

public List<ExcelErrorMessage> getErrorList() {

return listener.getErrorList();

}

/**

* 手动添加错误

*

* @param index data的下标(从0开始)

* @param errorMessage 错误信息

*/

public void addError(Integer index, String errorMessage) {

// 下标从0开始+1,标题占一行+1,总计+2

Integer row = index + 2;

listener.addError(row, errorMessage);

}

/**

* 生成错误信息excel,在原excel文件追加错误列

*

* @param filePath 源文件路径

*/

public Boolean generateErrorSheet(String filePath) {

List<ExcelErrorMessage> errorList = listener.getErrorList();

if (CollectionUtils.isEmpty(errorList)) {

return false;

}

Map<Integer, String> errorMap = errorList.stream().collect(Collectors.groupingBy(ExcelErrorMessage::getRowNum,

Collectors.mapping(ExcelErrorMessage::getMessage, Collectors.joining(";"))));

Workbook workbook = null;

// 打开原excel文件

try (

FileInputStream inputStream = new FileInputStream(filePath)) {

workbook = new XSSFWorkbook(inputStream);

Sheet sheet = workbook.getSheetAt(sheetNo);

// 添加错误列

Row headerRow = sheet.getRow(0);

short lastCellNum = headerRow.getLastCellNum();

// 检查是否已经存在错误列

Cell lastValidCell = headerRow.getCell(lastCellNum - 1);

if (lastValidCell != null) {

if (!ERROR_COLUMN_NAME.equals(lastValidCell.getStringCellValue())) {

Cell errorHeaderCell = headerRow.createCell(lastCellNum);

errorHeaderCell.setCellValue(ERROR_COLUMN_NAME);

errorMap.forEach((rowNum, msg) -> {

Row row = sheet.getRow(rowNum - 1);

if (row != null) {

Cell errorCell = row.createCell(lastCellNum);

errorCell.setCellValue(msg);

}

});

} else {

int lastRowNum = sheet.getLastRowNum();

for (int rowNum = 1; rowNum <= lastRowNum; rowNum++) {

Row row = sheet.getRow(rowNum);

String setErrorMsg = errorMap.get(rowNum + 1);

// 如果没有需要设置的错误信息,要把旧的错误信息清除

Cell errorCell = row.getCell(lastCellNum - 1);

if (setErrorMsg == null) {

if (errorCell != null) {

errorCell.setCellValue((String) null);

}

} else {

if (errorCell == null) {

errorCell = row.createCell(lastCellNum - 1);

}

errorCell.setCellValue(setErrorMsg);

}

}

}

}

} catch (IOException e) {

log.error("生成错误信息失败,", e);

throw new RuntimeException("生成错误信息失败");

}

try (FileOutputStream outputStream = new FileOutputStream(filePath)) {

// 写回去

workbook.write(outputStream);

workbook.close();

} catch (IOException e) {

log.error("生成错误信息失败,", e);

throw new RuntimeException("生成错误信息失败");

}

return true;

}

public static boolean isExcel2007(String filePath) {

return filePath.matches("^.+\\.(?i)(xlsx)$");

}

/**

* 验证EXCEL文件

*

* @param file

* @return

*/

public static void validateExcel(MultipartFile file) {

if (file == null) {

throw new RuntimeException("文件为空!");

}

String fileName = file.getOriginalFilename();

if (fileName != null && !isExcel2007(fileName)) {

throw new RuntimeException("导入文件必须是xlsx格式!");

}

if (StringUtils.isEmpty(fileName) || file.getSize() == 0) {

throw new RuntimeException("文件内容不能为空");

}

}

}

捕获类型转换异常

导入的第一步就是处理字段类型错误,因为如果出现类型转换错误,会直接导致程序异常,这里通过try,catch捕获ExcelAnalysisException异常来获取出现错误的列和行。

在这里插入图片描述

这里通过exception对象获取到了field,再获取字段上的ExcelProperty注解。

在AnalysisEventListener中实现校验逻辑

在listener中的<code>invoke方法中为每一行数据做校验,这里主要使用了反射

在这里插入图片描述

获取到Error后,根据错误信息生成Excel

这里是拿导入的原本Excel文件,在最后追加一列错误信息列,并将错误信息与行对应,代码如下

在这里插入图片描述



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。