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

news2024/11/19 15:13:59

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

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

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

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

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.coloradmin.cn/o/1491902.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

OJ_子串计算

题干 c实现 #include <stdio.h> #include <string> #include <map>using namespace std;int main() {char strArr[100];while (scanf("%s", strArr) ! EOF) {string str strArr;map<string, int> subCount;for (int i 0; i < str.size…

【[STM32]标准库-自定义BootLoader】

[STM32]标准库-自定义BootLoader BootloaderBootloader的实现BOOTloader工程APP工程 Bootloader bootloader其实就是一段启动程序&#xff0c;它在芯片启动的时候最先被执行&#xff0c;可以用来做一些硬件的初始化或者用作固件热更新&#xff0c;当初始化完成之后跳转到对应的…

C++基础2:C++基本数据类型和控制结构

此专栏为移动机器人知识体系下的编程语言中的 C {\rm C} C从入门到深入的专栏&#xff0c;参考书籍&#xff1a;《深入浅出 C {\rm C} C》(马晓锐)和《从 C {\rm C} C到 C {\rm C} C精通面向对象编程》(曾凡锋等)。 2.C基本数据类型和控制结构 2.1 C基本数据类型 程序是由算法…

这回就好好聊聊Kotlin的泛型

公众号「稀有猿诉」 原文链接 这回就好好聊聊Kotlin的泛型 泛型(Generics)是静态强类型编程语言中非常强大的特性&#xff0c;可以极大的加强代码的复用&#xff0c;并增强类型安全&#xff0c;减少运行时的类型转换错误。在这篇文章就来详细的学习一下Kotlin中对泛型的…

080|为什么阿里的价值观值得你关注?

在阿里巴巴20周年年会现场&#xff0c;万众瞩目之下&#xff0c;马云和张勇完成了阿里巴巴董事长职务的交接。 不过你也知道&#xff0c;这次接棒在一年前就已经公布了&#xff0c;在年会上只是一个仪式。在20周年年会过后&#xff0c;我找到了互联网圈的资深媒体人阳淼&#…

Mathcad tips_table相关

1. 可以插入表格&#xff0c;或者2. 从excel 文件导入 选择列 选择其中一行的数值

408专业课130+|我的备考经验和复盘

408的四门课任务量多到爆炸&#xff01;但难度不止于此。别忘了大部分选计算机的勇士们&#xff0c;是要考数学的&#xff01;直接起飞。 408数学无疑是王炸王炸&#xff0c;要想上岸就一定要把这两个大头一起拿下&#xff01; 作为一个成功上岸的非计算机专业跨考生&#xf…

计算机网络实验一 网线制作

实验目的与要求&#xff1a; 实验目的 了解以太网网线&#xff08;双绞线&#xff09;和制作方法 实验内容 了解网线和水晶头 学习网线制作方法 实验环境和要求 网线 水晶头 压线钳 剥线钳 网线测试器 方法、步骤&#xff1a; 步骤一 准备工具和材料 步骤二 剥掉双绞线的外…

16 PyTorch 神经网络基础【李沐动手学深度学习v2】

要想直观地了解块是如何工作的&#xff0c;最简单的方法就是自己实现一个。 在实现我们自定义块之前&#xff0c;我们简要总结一下每个块必须提供的基本功能。 将输入数据作为其前向传播函数的参数。 通过前向传播函数来生成输出。请注意&#xff0c;输出的形状可能与输入的形…

leetcode 热题 100_找到字符串中所有字母异位词

题解一&#xff1a; 滑动窗口&#xff1a;类似于字符串匹配&#xff0c;但匹配异位词需要包含相同的字母及个数&#xff0c;可以分别用两个数组存储字符串s滑动窗口和字符串p的字母及个数&#xff0c;再用Array.equals()进行比对。对于s.length()<p.length()的情况需要特判。…

2024 全国水科技大会暨污泥处理处置与资源化利用技术论坛(九)

为大会征集“绿色低碳污水厂案例”&#xff0c;欢迎各相关单位积极报名&#xff01; 一、会议背景 为深入学习贯彻《中共中央、国务院关于全面推进美丽中国建设的意见》&#xff0c;全面贯彻实施《固体废物污染环境防治法》、《“十四五”全国城市基础设施建设规划》&#xff0…

【操作系统概念】 第2章:操作系统结构

文章目录 0. 前言2.1 操作系统的服务2.2 操作系统的用户界面2.3 系统调用&#xff08;System Call&#xff09;2.3.1 系统调用的过程 2.4 系统调用类型2.5 系统程序分类2.6 操作系统设计和实现2.7 操作系统结构2.7.1 简单结构2.7.2 分层方法2.7.3 微内核2.7.4 模块 2.8 操作系统…

IO多路复用:2024/3/5

作业1、使用poll实现tcp服务器端&#xff0c;select实现tcp客户端 服务器端&#xff1a; #include <myhead.h> #define SER_IP "192.168.199.131" //服务端IP #define SER_PORT 8888 //服务端端口号int main(int argc, const char *argv[])…

【大数据】通过 docker-compose 快速部署 MinIO 保姆级教程

文章目录 一、概述二、MinIO 与 Ceph 对比1&#xff09;架构设计对比2&#xff09;数据一致性对比3&#xff09;部署和管理对比4&#xff09;生态系统和兼容性对比 三、前期准备1&#xff09;部署 docker2&#xff09;部署 docker-compose 四、创建网络五、MinIO 编排部署1&…

第一节 JDBC是什么?

JDBC代表Java数据库连接(Java Database Connectivity)&#xff0c;它是用于Java编程语言和数据库之间的数据库无关连接的标准Java API&#xff0c;换句话说&#xff1a;JDBC是用于在Java语言编程中与数据库连接的API。 JDBC库包括通常与数据库使用相关&#xff0c;如下面提到的…

基于Vue3的在线考试系统

TDuckX 是一个功能强大的可私有化部署的在线表单考试平台&#xff0c;可以帮助您轻松创建表单和在线考试。本文档将指导您如何使用 TDuckX 创建您自己的在线考试。 步骤 1&#xff1a;登录账户 完成系统部署后&#xff0c;在浏览器中打开 TDuckX 的网站。 如果您已经拥有账户&…

docker单节点搭建在线商城

本文档使用到的软件包以上传到资源中 目录 1. 创建容器并配置基础内容 1.1 将gpmall-repo上传到容器中 1.2 添加yum源 2. 安装基础服务 2.1 安装JAVA环境 2.2 安装Redis缓存服务 2.3 安装Elasticsearch服务 2.4 安装Nginx服务 2.5 安装MariaDB数据库 2.6 安…

积分商城管理系统的设计与实现(含源文件)

项目源码&#xff1a;https://gitee.com/oklongmm/biye2 系统介绍&#xff1a; 积分商城管理系统&#xff0c;包括用户模块、商品模块、积分模块和后台管理模块。 一、用户模块&#xff1a; 用户注册与登录&#xff1a;用户可以创建账户并登录系统。 个人信息管理&#xff1…

android开发教程视频,android组件化和插件化

第一阶段&#xff1a;Android 基础知识回顾&#xff1a; 回顾Android 开发编程&#xff0c;深入理解Android系统原理和层次结构&#xff0c;深入分析Handler源码和原理&#xff1b;回顾Java&#xff0c;C/C&#xff0c;Kotlin、dart 在Android开发中必用的语言&#xff0c;熟悉…

四平方和 刷题笔记

/* 四平方和 直接暴力搜索 可能会超时 使用二分辅助搜索 先枚举出 c*cd*d并存入数组 用式子算出 a*ab*b还剩下多少查找sum数组里面是否存在符合条件的数 查找方式使用二分搜索 当逼近答案后 检查一下是否为所需的数 如果是 直接输出 */ #include <cstring> #includ…