目录
- 功能设计
- 设计思路
- 代码解析
- 捕获类型转换异常
- 在AnalysisEventListener中实现校验逻辑
- 获取到Error后,根据错误信息生成Excel
功能设计
由于项目中涉及到大量的文件导入功能,故考虑设计一个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做的工作其实很简单,就是把文件中的内容映射到我们实体类的字段上,我们要做的就是在映射前和映射后做校验
代码解析
我先把完整代码贴上,下面再详细分析
注解类
/*** 导入校验注解** @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<>();/*** 错误信息集合*/@Getterprivate final List<ExcelErrorMessage> errorList = new ArrayList<>();private Boolean isEmpty = false;public CheckableImportListener() {super();}@Overridepublic 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());}}@Overridepublic 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);}}@Overridepublic 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,总计+2Integer 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中的invoke
方法中为每一行数据做校验,这里主要使用了反射
获取到Error后,根据错误信息生成Excel
这里是拿导入的原本Excel文件,在最后追加一列错误信息列,并将错误信息与行对应,代码如下