在前段时间的开发工作中,接手了一个很简单,很普通的开发任务。
要求实现一个单表的基础数据的批量导入功能。
评估下来,用户每次批量导入的数据量也就几千条,也不大。
是不是很简单,没有骗你们吧。但是呢,我实际去看的时候发现,好家伙,表里竟然一百多个字段,全部是需要导入的。
PS:表字段过多为什么没有分表的问题属于历史遗留问题,这里不做评判。
并且我遍寻整个项目,却没有找到处理批量导入的公共方法,相似功能全部都是if...else...!!!???
当时我的心理活动是这样的:
:???
:我*,不是吧,这咋搞。
:我总不能去写一百多个判断吧?这样搞估计能被锤死,在我写那么多判断好累的呀!!!
于是我果断仿照。。。不行,不能果断!
于是我就给项目简单写了批量导入的公共方法。
2思路
对于导入数据的校验来说,核心其实只有几个方面:
-
必填校验
-
判空
-
格式,包含email,电话,身份证等特殊格式,长度等
-
与excel列的对应关系
-
字典:需要将导入数据中的内容转成字典值入库
-
index:和cell对应关系
-
实体类数据组装
-
校验失败提示
其实,我们写的每一个if判断,都是在做同一个事情。那吗,针对这个场景,我们就可以采用注解+反射的方式来解决。
3开搞
自定义注解
首先,我们需要添加一个自定义注解。该注解主要标记相应字段与cell的对应关系以及需要进行的处理。(PS:上面提到的特殊格式的校验,这里没有做实现,需要的增加一个字段保存正则表达式即可)
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface ImportValidation {
//下标,与excel中列对应,从0开始
int index();
//是否必填,默认是必填
boolean nullAble() default true;
// 字典的Code,用于字典转换
String domainCode() default "";
//字典的名称,用于错误提醒
String name() default "";
}
定义一个公共的静态方法
改公共方法需要包含三个参数:
-
class
:用于组装数据 -
Map<Integer,String[]>
:我这里是将excel的内容全部读取出来保存在了Map中。 -
domainCodes
:所有涉及的字段转换,调用方应将字段按照code组装成Map的形式以供使用
public static Result assembleExcelData(Class entryClass, Map<Integer, String[]> excelData,
Map<String,Object> domainCodes){
....
数据组装
这里直接看代码
public static Result assembleExcelData(Class entryClass, Map<Integer, String[]> excelData,
Map<String,Object> domainCodes){
//保存返回的结果
Result result = new Result();
//组装后的数据LIST
List<Object> returnList = new ArrayList<>();
//保存校验失败信息
StringBuilder errorMsg = new StringBuilder();
//循环excel数据
excelData.forEach((i,cells)->{
Object vo = null;
try {
//按照传入的Class,生成对应实例
vo= entryClass.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
//获取并循环Bean中的所有字段,进行校验和组装
for (Field field : entryClass.getDeclaredFields()) {
//如果包含有ImportValidation注解的话,才进行处理。
if (field.isAnnotationPresent(ImportValidation.class)) {
ImportValidation annotation = field.getAnnotation(ImportValidation.class);
//cell下表
int index = annotation.index();
//字典Code
String domainCode = annotation.domainCode();
//是否必填
boolean nullAble = annotation.nullAble();
//字段名称
String name = annotation.name();
//获取单元格内容,并前后去空格处理
String cellData = cells[index].trim();
/*如果字段为空,且字段设置不能为空,则进行错误提醒*/
try {
//若必填,则进行判断校验并提醒
if (StringUtils.isEmpty(cellData) && !nullAble) {
errorMsg.append("第").append(i).append("行: ").append(name).append("字段不能为空!\r\n");
}
/*如果字典编码为空,则可以直接赋值*/
else if (StringUtils.isEmpty(domainCode) || StringUtils.isEmpty(cellData)) {
//给对应字段赋值
setFiled(field, vo, cellData);
} else {
//进行字典转换
List<Map> domains = (List<Map>) domainCodes.get(domainCode);
boolean match = false;
for (Map map : domains) {
if (map.get("TEXT").equals(cellData)) {
//给对应字段赋值
setFiled(field, vo, String.valueOf(map.get("VALUE")));
match = true;
break;
}
}
/*如果没有匹配,则转换失败*/
if (!match) {
errorMsg.append("第").append(i).append("行: ").append(name).append("字段字典值不存在!!\r\n");
}
}
} catch (Exception e) {
errorMsg.append("第").append(i).append("行: ").append(name).append("字段填写格式不正确!!\r\n");
}
}
}
//组装LIST
returnList.add(vo);
});
//如果有错误信息的话,返回错误信息,返回错误标记
if (errorMsg.length()>0){
result = Result.buildError();
result.setMsg(errorMsg.toString());
}
//放入组装后的LIST。校验失败的字段值为空
result.setData(returnList);
return result;
}
//反射给Filed赋值
public static void setFiled(Field filed,Object vo,String data) throws IllegalAccessException {
try {
//当单元格值不为空的时候才需要进行赋值操作
if (StringUtils.isNotEmpty(data)){
//获取Bean 属性字段的类型
Type fileType = filed.getGenericType();
filed.setAccessible(true);
//如果是String
if (fileType.equals(String.class)){
filed.set(vo,data);
}
//如果是int
else if(fileType.equals(int.class)||fileType.equals(Integer.class)){
filed.set(vo,Integer.valueOf(data));
}
//如果是Double
else if(fileType.equals(Double.class)||fileType.equals(double.class)){
filed.set(vo,Double.valueOf(data));
}
//如果是Long
else if(fileType.equals(Long.class)||fileType.equals(long.class)){
filed.set(vo,Long.valueOf(data));
}
//如果是BigDecimal
else if(fileType.equals(BigDecimal.class)){
filed.set(vo,new BigDecimal(data));
}
//如果是日期
else if(fileType.equals(Date.class)){
filed.set(vo, DateUtils.parseIso8601DateTime(data));
}
}
} catch (Exception e) {
throw e;
}
}
使用
我这里如果校验失败的话是给前端返回一个错误提醒内容的txt文件。可自行根据项目情况处理。校验成功则做插入的操作。
String domainCodesStr = "MM_DIC_PART_ATTR,MM_DIC_PART_TYPE,MM_DIC_PART_BELONG,MM_DIC_BASE_UNIT," +
"MM_DIC_PART_SOURCE,MM_DIC_W_UNIT,MM_MIN_SHELF_LIFE_UNIT,MM_CURRENCY";
/*查询相关字典,进行校验和转换*/
Map<String, Object> domainsCodes = wsDataDomainService.getDataByDomainCodes(domainCodesStr.split(","));
/*校验并组装数据*/
Result result = ExcelUtils.assembleExcelData(MmPartNumber.class, excelData, domainsCodes);
if (result.getCode() != 0) {
String realPath = SpringContextHolder.getServletContext().getRealPath("/");
String destination = realPath + "导入错误信息.txt";
/*返回错误信息文件*/
File file = new File(destination);
if (!file.exists()) {
file.createNewFile();
}
FileWriter fileWriter = new FileWriter(file);
fileWriter.write(result.getMsg());
fileWriter.close();
HttpServletResponse response = context.getHttpServletResponse();
FileDownload.fileDownload(response, realPath + "导入错误信息.txt", "导入错误信息.txt");
} else {
//TODO BatchInsert
}
效果
4总结
通过自定义注解+反射的方式,实现对批量导入数据的校验及组装。
这是一个非常常规和简单的实现方式。
不得不说,SpringBoot自定义注解真的是个好东西。
如果有类似这种重复工作的场景,不妨多考虑考虑,是否可以通过该机制实现.
最后说一句(求关注!别白嫖!)
如果这篇文章对您有所帮助,或者有所启发的话,求一键三连:点赞、转发、在看。
关注公众号:woniuxgg,在公众号中回复:笔记 就可以获得蜗牛为你精心准备的java实战语雀笔记,回复面试、开发手册、有超赞的粉丝福利!