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

功能设计

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

  1. 导入的数据不可信任,可能出现空值校验的许多判断,如果将这些判断加入业务代码可能会造成大量代码的堆积,如下情况:
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");
}

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

  2. 针对文件中的问题,需要清晰地呈现给用户,每一行具体出现了哪种类型的错误,例如如下:
    在这里插入图片描述

  3. 基于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<>();
    /**
     * 错误信息集合
     */
    @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中的invoke方法中为每一行数据做校验,这里主要使用了反射

在这里插入图片描述

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

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

  • 6
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值