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