在EasyExcel下导入Excel,解放if参数校验

前言

在部分场景中用Java操作Excel,相信很多人都有遇到过这个情况。阿落在前期用POI比较多,POI好不好用暂且略过不表,因为这不是我们今天的主题。有段时间,看到推荐说EasyExcel对于Excel的导入导出做了很大的简化与便利,便引入到项目中试一试。

今天我们的主题是讲使用EasyExcel对Excel进行导入操作,对于一些参数校验上的解放,咱们接着往下瞧。

(要看目前最终解决方案的,请拖到最下面。)

(0)前提信息

①使用的easy-excel版本(maven依赖)



  com.alibaba
  easyexcel
  2.1.3

②一些类或者封装

RestResult:封装的返回数据结构(如success、msg、data等),此处是返回业务层的操作成功与失败记录;

ThrowJsonException:继承RuntimeException的异常,通过全局异常捕获抛出;

(1)0.1版本参数校验

①控制层入口

@RequestMapping(value = "/import")
@ResponseBody
public RestResult handleImport(@RequestParam(name = "excelFile") MultipartFile excelFile) {
  ...
  List readList = EasyExcelReadUtil.importTemp(inputStream);
  if (CollectionUtils.isEmpty(readList)) {
    throw new ThrowJsonException("上传的文件获取内容失败");
  }
  ...
  //拿到了上述的`readList`后,可以去对应的业务层进行其他操作(如插入更新等)
  return null;
}

②导入使用的JavaBean

import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.metadata.BaseRowModel;
import lombok.Getter;
import lombok.Setter;

import java.io.Serializable;

/**
 * ImportTempVO
 * 

通过index来确定单元格的位置

* * @author 阿落学Java */ @Getter @Setter public class ImportTempVO extends BaseRowModel implements Serializable { private static final long serialVersionUID = -8157208126839648562L; @ExcelProperty(value = "编号(最大10位)", index = 0) private String code; @ExcelProperty(value = "姓名(最大10位)", index = 1) private String name; @ExcelProperty(value = "性别(1男2女)", index = 2) private Integer gender; @ExcelProperty(value = "年龄(纯数字)", index = 3) private Integer age; @ExcelProperty(value = "手机号(确保格式正确)", index = 4) private String phone; @ExcelProperty(value = "学校ID(确保与学校对应)", index = 5) private Integer schoolId; }

③读取excel文件,转换为List

import com.alibaba.excel.EasyExcel;
import com.sanxin.education.core.pojo.vo.reporter.ReporterTempVO;

import java.io.InputStream;
import java.util.List;

/**
 * 阿里云easyexcel导入读取工具类
 *
 * @author 阿落学Java
 */
public class EasyExcelReadUtil {

    public static List importTemp(InputStream inputStream) {
        List list = EasyExcel.read(inputStream)
                // 这个转换是成全局的, 所有java为string,excel为string的都会用这个转换器。
                // 如果就想单个字段使用请在(0)的②的JavaBean的属性上使用`@ExcelProperty`指定converter
                .registerConverter(new StringConverter())
                // 注册监听器,可以在这里校验字段【填入excel表格数据的数据校验】
                .registerReadListener(new EasyExcelImportListener())
                .head(ImportTempVO.class)
                .sheet()
                .headRowNumber(1)
                .doReadSync();
        return list;
    }
}

④EasyExcelImportListener数据校验

import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.alibaba.excel.exception.ExcelDataConvertException;
import com.sanxin.education.common.enums.base.GenderEnum;
import com.sanxin.education.common.exception.ThrowJsonException;
import com.sanxin.education.common.util.regular.RegularUtil;
import com.sanxin.education.core.pojo.vo.reporter.ReporterTempVO;
import org.apache.commons.lang3.StringUtils;

import java.util.ArrayList;
import java.util.List;

/**
 * 阿里云easyexcel导入数据校验监听类
 *
 * @author 阿落学Java
 */
@SuppressWarnings("all")
public class EasyExcelImportListener extends AnalysisEventListener {

    List orderCodeList = new ArrayList<>();

    /**
     * 每解析一行,回调该方法
     */
    @Override
    public void invoke(Object data, AnalysisContext context) {
        //正则表达式工具类 此处是为了校验手机号格式
        RegularUtil regularUtil = RegularUtil.getInstance();

        String code = ((ImportTempVO) data).getCode();
        if (StringUtils.isBlank(code)) {
            throw new ThrowJsonException(String.format("第%s行编号为空,请核实", context.readRowHolder().getRowIndex() + 1));
        }
        if (code.length() > 10) {
            throw new ThrowJsonException(String.format("第%s行编号过长,请核实", context.readRowHolder().getRowIndex() + 1));
        }
        String name = ((ReporterTempVO) data).getName();
        if (StringUtils.isBlank(name)) {
            throw new ThrowJsonException(String.format("第%s行名称为空,请核实", context.readRowHolder().getRowIndex() + 1));
        }
        Integer gender = ((ReporterTempVO) data).getGender();
        if (gender == null) {
            throw new ThrowJsonException(String.format("第%s行性别为空,请核实", context.readRowHolder().getRowIndex() + 1));
        }
        if (!GenderEnum.isValidGender(gender)) {
            throw new ThrowJsonException(String.format("第%s行性别填写有误,请核实", context.readRowHolder().getRowIndex() + 1));
        }
        Integer age = ((ReporterTempVO) data).getAge();
        if (age == null) {
            throw new ThrowJsonException(String.format("第%s行年龄为空,请核实", context.readRowHolder().getRowIndex() + 1));
        }
        String phone = ((ReporterTempVO) data).getPhone();
        if (StringUtils.isBlank(phone)) {
            throw new ThrowJsonException(String.format("第%s行手机号为空,请核实", context.readRowHolder().getRowIndex() + 1));
        }
        String validPhone = regularUtil.validPhone(phone);
        if (StringUtils.isNotBlank(validPhone)) {
            throw new ThrowJsonException(String.format("第%s行手机号格式不正确,请核实", context.readRowHolder().getRowIndex() + 1));
        }
        Integer schoolId = ((ReporterTempVO) data).getSchoolId();
        if (schoolId == null) {
            throw new ThrowJsonException(String.format("第%s行学校ID为空,请核实", context.readRowHolder().getRowIndex() + 1));
        }
    }

    @Override
    public void onException(Exception exception, AnalysisContext context) throws Exception {
        // ExcelDataConvertException:当数据转换异常的时候,会抛出该异常,此处可以得知第几行,第几列的数据
        if (exception instanceof ExcelDataConvertException) {
            Integer columnIndex = ((ExcelDataConvertException) exception).getColumnIndex() + 1;
            Integer rowIndex = ((ExcelDataConvertException) exception).getRowIndex() + 1;
            String message = String.format("第%s行,第%s列数据格式有误,请核实", rowIndex, columnIndex);
            throw new ThrowJsonException(message);
        } else if (exception instanceof RuntimeException) {
            throw exception;
        } else {
            super.onException(exception, context);
        }
    }

    /**
     * 解析完全部回调
     */
    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        orderCodeList.clear();
    }
}

至此,通过EasyExcel导入excel,并将其转换为Java中的List已经完成,可以看到在③中对数据校验都是通过if-else的方式进行校验,且部分情况下可能存在与数据库的数据进行比对的时候,在业务层还需要对数据进行一次核验,在实际应用中确实多有不便。

(2)0.5版本参数校验

①控制层

@RequestMapping("/import")
public void read(MultipartFile file) throws IOException {
  //两个入参,一个是service的名称,动态代理获取去操作业务,一个是methodName方法名,在你的业务层接口及实现类中的方法
  DemoDataListener demoDataListener = new DemoDataListener("demoImportService","saveBatch");
  ImportUtil.read(file.getInputStream(), DemoImport.class, demoDataListener);
}

②导入使用的JavaBean,与(1)中的情况有所不同

import com.alibaba.excel.annotation.ExcelProperty;
import com.crosscloud.common.annotation.excel.ExcelDecimalValid;
import com.crosscloud.common.annotation.excel.ExcelIntValid;
import com.crosscloud.common.annotation.excel.ExcelStrValid;
import com.crosscloud.common.annotation.excel.ExcelValid;
import lombok.Data;

import java.math.BigDecimal;

/**
 * 

导入Demo类

*

@ExcelProperty(index = 0) 读下标为0的单元格数据,即第一个单元格

*

@ExcelProperty("字符串标题") 读表头相同的单元格数据,如果名字重复,会导致只有一个字段读取到数据

*

这里不建议 index 和 name 同时用,要么一个对象只用index,要么一个对象只用name去匹配

* * @author 阿落学Java */ @Data public class DemoImport { /** * 读第一个单元格 */ @ExcelProperty(index = 0) @ExcelValid(message = "no不能为空") @ExcelIntValid(min = 1, max = 3, message = "no最小为1,最大为3") private Integer no; @ExcelProperty(index = 1) @ExcelValid(message = "name不能为空") @ExcelStrValid(length = 3, message = "name最大为3个长度") private String name; @ExcelDecimalValid(min = "1", max = "3", message = "价格1最小为1最大为3") private BigDecimal price1; }

看到这里相信很多小伙伴会发现在这个JavaBean里面多了一些注解吧,没错,就是通过注解的方式来进行参数校验。

③注解

这是阿落会用到的一些注解,各位小伙伴可以根据实际情况增减注解。

/**
 * 

Excel导入必填校验注解

* * @author 阿落学Java */ @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface ExcelValid { String message() default "导入有未填入的字段"; } /** *

Excel导入字符串长度校验

* * @author 阿落学Java */ @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface ExcelStrValid { int length() default 0; String message() default "文字填写超出长度要求"; } /** *

Excel导入Int类型校验

* * @author 阿落学Java */ @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface ExcelIntValid { int min(); int max(); String message() default "整数数字填写超出范围"; } /** *

Excel导入Decimal类型校验

* * @author 阿落学Java */ @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface ExcelDecimalValid { String min(); String max(); String message() default "小数类型数字填写超出范围"; }

④导入监听类

package com.crosscloud.excel.listener;

import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.alibaba.excel.exception.ExcelDataConvertException;
import com.crosscloud.common.exception.BaseExceptionCode;
import com.crosscloud.common.exception.InfoCode;
import com.crosscloud.common.exception.ThrowJsonException;
import com.crosscloud.common.utils.SpringUtil;
import com.crosscloud.excel.constant.ExcelConstant;
import com.crosscloud.excel.valid.ExcelImportValid;
import com.crosscloud.model.dto.demo.DemoImport;
import lombok.extern.slf4j.Slf4j;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

/**
 * 

EasyExcel监听的demo

* * @author leehao * @date 2021/6/1 */ @Slf4j public class DemoDataListener extends AnalysisEventListener { //service的名称 private String serviceName; //方法名 阿落是只要涉及导入操作,都会在业务层接口和实现类定义方法为`saveBatch` //这样可以在方法中扩展比如不存在则插入 private String methodName; List list = new ArrayList(); //构造函数,也是控制层进来的时候需要传入两个参数 public DemoDataListener(String serviceName, String methodName) { this.serviceName = serviceName; this.methodName = methodName; } /** *

每一条数据解析后,调用此方法

*/ @Override public void invoke(DemoImport data, AnalysisContext context) { //通用方法数据校验 ExcelImportValid.valid(data); //将解析完的数据加入到list中 list.add(data); // 达到BATCH_COUNT了,需要去存储一次数据库,防止数据几万条数据在内存,容易OOM if (list.size() >= ExcelConstant.READ_BATCH_COUNT_10) { this.execute(); // 存储完成清理 list list.clear(); } } /** *

所有数据解析完成后调用此方法

*/ @Override public void doAfterAllAnalysed(AnalysisContext context) { //保存数据,确保最后的遗留数据也能保存到数据库或者进行其他操作 this.execute(); } /** *

数据转换异常时的处理,比如required是Integer,provided是"sss"的字符串

*/ @Override public void onException(Exception exception, AnalysisContext context) throws Exception { if (exception instanceof ExcelDataConvertException) { Integer columnIndex = ((ExcelDataConvertException) exception).getColumnIndex() + 1; Integer rowIndex = ((ExcelDataConvertException) exception).getRowIndex() + 1; String message = String.format("第%s行,第%s列数据格式有误,请核实", rowIndex, columnIndex); throw new ThrowJsonException(InfoCode.IMPORT_PARAM_ERROR, message); } else if (exception instanceof RuntimeException) { throw exception; } else { super.onException(exception, context); } } //执行数据保存的操作 public void execute() { try { Object beanService = SpringUtil.getBean(serviceName + "Impl"); Class beanClass = beanService.getClass(); Method method = beanClass.getMethod(methodName, List.class); method.invoke(beanService, list); } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { log.warn("import error:", e); throw new ThrowJsonException(BaseExceptionCode.INTERNAL_SERVER_ERROR, "导入执行保存失败"); } } }

⑤ExcelImportValid参数校验

import com.xxx.common.annotation.excel.ExcelDecimalValid;
import com.xxx.common.annotation.excel.ExcelIntValid;
import com.xxx.common.annotation.excel.ExcelStrValid;
import com.xxx.common.annotation.excel.ExcelValid;
import com.xxx.common.exception.InfoCode;
import com.xxx.common.exception.ThrowJsonException;
import org.apache.commons.lang3.StringUtils;

import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.util.Objects;

/**
 * 

Excel导入字段校验

* * @author 阿落学Java */ public class ExcelImportValid { /** * Excel导入字段校验 * * @param object 校验的JavaBean 其属性须有自定义注解 * @author 阿落学Java */ public static void valid(Object object) { Field[] fields = object.getClass().getDeclaredFields(); for (Field field : fields) { //设置可访问 field.setAccessible(true); //属性的值 Object fieldValue = null; try { fieldValue = field.get(object); } catch (IllegalAccessException e) { throw new ThrowJsonException(InfoCode.IMPORT_PARAM_CHECK_FAIL, field.getAnnotation(ExcelValid.class).message()); } //是否包含必填校验注解 boolean isExcelValid = field.isAnnotationPresent(ExcelValid.class); if (isExcelValid && Objects.isNull(fieldValue)) { throw new ThrowJsonException(InfoCode.IMPORT_PARAM_CHECK_FAIL, field.getAnnotation(ExcelValid.class).message()); } //是否包含字符串长度校验注解 boolean isExcelStrValid = field.isAnnotationPresent(ExcelStrValid.class); if (isExcelStrValid) { String cellStr = fieldValue.toString(); int length = field.getAnnotation(ExcelStrValid.class).length(); if (StringUtils.isNotBlank(cellStr) && cellStr.length() > length) { throw new ThrowJsonException(InfoCode.IMPORT_PARAM_CHECK_FAIL, field.getAnnotation(ExcelStrValid.class).message()); } } //是否包含int类型校验注解 boolean isExcelIntValid = field.isAnnotationPresent(ExcelIntValid.class); if (isExcelIntValid) { if (fieldValue instanceof Integer) { int cellInt = Integer.parseInt(fieldValue.toString()); int min = field.getAnnotation(ExcelIntValid.class).min(); int max = field.getAnnotation(ExcelIntValid.class).max(); if (cellInt < min || cellInt > max) { throw new ThrowJsonException(InfoCode.IMPORT_PARAM_CHECK_FAIL, field.getAnnotation(ExcelIntValid.class).message()); } } } //是否包含decimal类型注解 boolean isExcelDecimalValid = field.isAnnotationPresent(ExcelDecimalValid.class); if (isExcelDecimalValid) { if (isBigDecimal(fieldValue.toString())) { BigDecimal cellDecimal = new BigDecimal(fieldValue.toString()); BigDecimal min = new BigDecimal(field.getAnnotation(ExcelDecimalValid.class).min()); BigDecimal max = new BigDecimal(field.getAnnotation(ExcelDecimalValid.class).max()); if (cellDecimal.compareTo(min) < 0 || cellDecimal.compareTo(max) > 0) { throw new ThrowJsonException(InfoCode.IMPORT_PARAM_CHECK_FAIL, field.getAnnotation(ExcelDecimalValid.class).message()); } } else { throw new ThrowJsonException(InfoCode.IMPORT_PARAM_CHECK_FAIL, "不是小数数字类型"); } } } } private static boolean isBigDecimal(String decimal) { try { BigDecimal bd = new BigDecimal(decimal); return true; } catch (NumberFormatException e) { return false; } } }

⑥ImportUtil工具类

import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.event.AnalysisEventListener;
import com.xxx.common.exception.ThrowJsonException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.io.InputStream;

/**
 * 

导入工具类 使用EasyExcel

* * @author 阿落学Java */ @Slf4j public class ImportUtil { public static void read(MultipartFile file, Class clazz, AnalysisEventListener listener) { try { EasyExcel.read(file.getInputStream(), clazz, listener).sheet().doRead(); } catch (Exception e) { log.warn("import file error: ", e); throw new ThrowJsonException("导入失败异常信息"); } } }

至此,导入就完成了,总结一下,这样导入的话,可以概括为这么几个步骤:

1.定义自己在导入业务开发中用到的注解,如必填注解、字符串长度注解等;

2.在导入的JavaBean的属性上添加对应的注解;

3.新建导入监听,继承AnalysisEventListener,传入操作数据的ServiceBean的名称与方法名;

4.在自己建立的监听类中invoke方法中使用ExcelImportValid.valid(data);对数据进行校验;

(3)0.x版本(> 0.5)

对于使用EasyExcel导入excel,仍在持续寻找方法优化,也欢迎大家有好的方法和建议一起探讨。

你可能感兴趣的:(在EasyExcel下导入Excel,解放if参数校验)