前言

在部分场景中用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,仍在持续寻找方法优化,也欢迎大家有好的方法和建议一起探讨。