⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 juejin.cn/post/7224695595197120572 「兰小胖」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

本次给大家带来的SpringBoot中通过自定义注解+反射实现excel导入数据组装及字段校验的实现方式。这种实现方式其实是很普通、常规的方法,但很多同学在开发过程中,可能却不太容易想到他。当然我也是众多同学中的一员。

1 背景

在前段时间的开发工作中,接手了一个很简单,很普通的开发任务。要求实现一个单表的基础数据的批量导入功能。

评估下来,用户每次批量导入的数据量也就几千条,也不大。是不是很简单,没有骗你们吧。但是呢,我实际去看的时候发现,好家伙,表里竟然一百多个字段,全部是需要导入的。

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自定义注解真的是个好东西。如果有类似这种重复工作的场景,不妨多考虑考虑,是否可以通过该机制实现。

5 最后

很多时候,技术就是层窗户纸,戳破了也就很简单了。不怕我们技术不会,最怕的是我们想不到~

文章目录
  1. 1. 1 背景
  2. 2. 2 思路
  3. 3. 3 开搞
    1. 3.0.1. 自定义注解
    2. 3.0.2. 定义一个公共的静态方法
    3. 3.0.3. 数据组装
    4. 3.0.4. 使用
    5. 3.0.5. 效果
  • 4. 4 总结
  • 5. 5 最后