SpringBoot自定义注解+反射实现 excel 导入的数据组装及字段校验

在前段时间的开发工作中,接手了一个很简单,很普通的开发任务。

要求实现一个单表的基础数据的批量导入功能。

评估下来,用户每次批量导入的数据量也就几千条,也不大。

是不是很简单,没有骗你们吧。但是呢,我实际去看的时候发现,好家伙,表里竟然一百多个字段,全部是需要导入的。

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实战语雀笔记,回复面试、开发手册、有超赞的粉丝福利!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/431769.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

常用工具——Gradle

前言 实践是最好的学习方式&#xff0c;技术也如此。 文章目录 前言一、Gradle 简介二、文件结构详解 一、Gradle 简介 Gradle 文件是一个独立于 android 之外的一个东西&#xff1b; 是什么 gradle 就是编译、打包 Android 工程的一个构建工具&#xff1b;build.gradle 文件&…

中仕公考:非应届生能考三支一扶吗?

如果是非应届生身份能参加三支一扶考试吗? “三支一扶”是一项公益性的就业计划&#xff0c;全称为“支持教育、支持农村、支持医疗和扶贫”。该计划主要是针对大学生毕业生设置的&#xff0c;通过招募他们到基层单位工作&#xff0c;以解决基层单位人才短缺的问题&#xff0…

MapReduce内存参数自动推断

MapReduce内存参数自动推断。在Hadoop 2.0中&#xff0c;为MapReduce作业设置内存参数非常繁琐&#xff0c;涉及到两个参数&#xff1a;mapreduce.{map,reduce}.memory.mb和mapreduce.{map,reduce}.java.opts&#xff0c;一旦设置不合理&#xff0c;则会使得内存资源浪费严重&a…

java中this关键字的使用

this关键字的使用 this的用法1&#xff09;this.data2&#xff09;this.method&#xff1b;3&#xff09;this() this的用法 1&#xff09;this.data&#xff1b; &#xff08;访问属性&#xff09; 2&#xff09;this.method&#xff1b; &#xff08;访问方法&#xff09; 3&…

wait() 、notify()、notifyAll() 的详细用法

文章目录 &#x1f490;wait() 讲解&#x1f490;notify() 讲解&#x1f490;notifyAll()&#x1f4a1;wait() 和 sleep() 的区别 首先&#xff0c;我们知道&#xff0c;线程的执行顺序是随机的(操作系统随机调度的&#xff0c;抢占式执行)&#xff0c;但是有时候&#xff0c;我…

软件测试面试题(全)

【软件测试面试突击班】2024吃透软件测试面试最全八股文攻略教程&#xff0c;一周学完让你面试通过率提高90%&#xff01;&#xff08;自动化测试&#xff09; 1.B/S架构和C/S架构区别 B/S 只需要有操作系统和浏览器就行&#xff0c;可以实现跨平台&#xff0c;客户端零维护&a…

CSS全局样式的设置,web开发交流

面试题 HTML 1&#xff0c;html5有哪些新特性&#xff1f; 2&#xff0c;html5移除了那些元素&#xff1f; 3&#xff0c;如何处理HTML5新标签的浏览器兼容问题 戳这里领取完整开源项目&#xff1a;【一线大厂前端面试题解析核心总结学习笔记Web真实项目实战最新讲解视频】…

智能硬件 | AI PC新市场,英特尔、高通、AMD、苹果谁能拔得头筹?

我们普通人和大模型的距离有多远&#xff1f;AI发展到2024年&#xff0c;已经附着在各种智能硬件上了&#xff0c;什么AI PC&#xff0c;AI手机&#xff0c;AI蓝牙音箱&#xff0c;AI学习机&#xff0c;AI鼠标等等&#xff0c;但其实虽然很多产品加上了个AI的名头&#xff0c;但…

xss.haozi:0x00

0x00没有什么过滤所以怎么写都没有关系有很多解 <script>alert(1)</script>

实现session共享的方法总结完整版

文章目录 实现session共享的方法总结完整版1、使用共享数据库&#xff1a;2、使用粘性会话&#xff08;Sticky Session&#xff09;&#xff1a;3、使用缓存系统&#xff1a;4、使用分布式文件系统&#xff1a;5、使用中央认证服务&#xff1a;6、使用会话复制&#xff1a;7、使…

LLM 模型量化推理速度评测

最近了解了下些常见的推理和加速方案&#xff1a; 1、量化方案&#xff1a; gptq、quantization、int8、int4、AWQ、Speculative Decoding、GGUF 2、Attention加速方案&#xff1a; atten的不同种类fused attention 3、内存层面&#xff1a; kv_cache策略、page_attention…

3.4作业

课上代码复习&#xff1a; 广播接收端代码: #include<myhead.h> int main(int argc, const char *argv[]) {//创建套接字int rfd socket(AF_INET,SOCK_DGRAM,0);if(rfd -1){perror("socket error");return -1;}printf("rfd %d\n",rfd);//填充地…

在 Flutter 中使用 flutter_gen 简化图像资产管理

你是否厌倦了在 Flutter 项目中手动管理图像资产的繁琐任务&#xff1f; 告别手工输入资源路径的痛苦&#xff0c;欢迎使用“Flutter Gen”高效资源管理的时代。在本文中&#xff0c;我将带您从手动处理图像资源的挫折到动态生成它们的便利。 选择1&#xff1a;痛苦手动添加–…

达梦数据库基础操作(五): 索引操作

达梦数据库基础操作(五)&#xff1a; 索引操作 1. 索引操作 1.1 创建索引 # 使用 CREATE INDEX 语句创建普通索引。 CREATE INDEX ind_emp_salary ON employee(salary);1.2 查看创建的索引 # 通过字典表 user_indexes 查看已创建索引的名称、类型。SELECT table_name, index…

C语言经典算法-1

C语言经典算法讲解练习 文章目录 C语言经典算法讲解练习1.汉若塔2.费式数列3. 巴斯卡三角形4.三色棋5.老鼠走迷官&#xff08;一&#xff09;6.老鼠走迷官&#xff08;二&#xff09;7.骑士走棋盘8.八皇后9.八枚银币10.生命游戏 1.汉若塔 说明&#xff1a;河内之塔(Towers of …

ArmSoM Rockchip系列产品 通用教程 之 UART 使用

1. UART 简介​ Rockchip UART (Universal Asynchronous Receiver/Transmitter) 基于16550A串口标准&#xff0c;完整模块支持以下功能&#xff1a; 支持5、6、7、8 bits数据位。支持1、1.5、2 bits停止位。支持奇校验和偶校验&#xff0c;不支持mark校验和space校验。支持接…

文物保护平台数据统计分析及预警-子系统专题分析

文物预防性监测与调控系统的监测统计分析子系统提供全面的文物状态及环境数据分析,为博物馆工作人员进行基于文物材质特性的专项保护提供相关科研辅助。主要的监测分析,包括各展厅文物统计分析、不同环境因素报表统计、以及监测调控设备统计分析等。 系统用户和文物管理人员可以…

onnx runtime文档学习2-torch TF简单示例

网上充斥着ONNX Runtime的简单科普&#xff0c;却没有一个系统介绍ONNX Runtime的博客&#xff0c;因此本博客旨在基于官方文档进行翻译与进一步的解释。ONNX runtime的官方文档&#xff1a;https://onnxruntime.ai/docs/ 如果尚不熟悉ONNX格式&#xff0c;可以参照该博客专栏…

Nodejs 第四十七章(redis主从复制)

Redis主从复制是一种数据复制和同步机制&#xff0c;其中一个Redis服务器&#xff08;称为主服务器&#xff09;将其数据复制到一个或多个其他Redis服务器&#xff08;称为从服务器&#xff09;。主从复制提供了数据冗余备份、读写分离和故障恢复等功能。 以下是Redis主从复制的…

redis06 redis事务

思维草图 redis事务认识 redis事务是一个单独的隔离操作&#xff0c;事务中的所有命令都会序列化、按顺序地执行&#xff0c;事务在执行的过程中&#xff0c;不会被其他客户端发送来的命令请求所打断。 redis事务的主要作用就是串联多个命令防止别的命令插队。 Multi、Exec、…
最新文章