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

lxf2023-12-20 13:10:01

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

题外话

在之前的文章中,我讲到了Springboot中利用自定义注解+反射机制,通过实现BeanPostProcessorpostProcessBeforeInitialization接口的方式,实现了配置类自动注入的问题。有兴趣的同学可以去看一看:

《SpringBoot通过自定义注解实现配置类的自动注入 》

背景

在前段时间的开发工作中,接手了一个很简单,很普通的开发任务。要求实现一个单表的基础数据的批量导入功能。评估下来,用户每次批量导入的数据量也就几千条,也不大。是不是很简单,没有骗你们吧。但是呢,我实际去看的时候发现,好家伙,表里竟然一百多个字段,全部是需要导入的(PS:表字段过多为什么没有分表的问题属于历史遗留问题,这里不做评判)。并且我遍寻整个项目,却没有找到处理批量导入的公共方法,相似功能全部都是if...else...!!!???

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

当时我的心理活动是这样的:

:???
:我*,不是吧,这咋搞。
:我总不能去写一百多个判断吧?这样搞估计能被锤死,在我写那么多判断好累的呀!!!

于是我果断仿照。。。不行,不能果断!于是我就给项目简单写了批量导入的公共方法。

思路

对于导入数据的校验来说,核心其实只有几个方面:

  1. 必填校验
  2. 判空
  3. 格式,包含email,电话,身份证等特殊格式,长度等
  4. 与excel列的对应关系
  5. 字典:需要将导入数据中的内容转成字典值入库
  6. index:和cell对应关系
  7. 实体类数据组装
  8. 校验失败提示

其实,我们写的每一个if判断,都是在做同一个事情。那吗,针对这个场景,我们就可以采用注解+反射的方式来解决。

开搞

自定义注解

首先,我们需要添加一个自定义注解。该注解主要标记相应字段与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  "";

}

定义一个公共的静态方法

改公共方法需要包含三个参数:

  1. class:用于组装数据
  2. Map<Integer,String[]>:我这里是将excel的内容全部读取出来保存在了Map中。
  3. 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
}

效果

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

总结

通过自定义注解+反射的方式,实现对批量导入数据的校验及组装。这是一个非常常规和简单的实现方式。不得不说,SpringBoot自定义注解真的是个好东西。如果有类似这种重复工作的场景,不妨多考虑考虑,是否可以通过该机制实现。

最后

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

感谢JRM进行浏览,希望能给一些同学带来帮助,哪怕一个也好。全剧终~

本网站是一个以CSS、JavaScript、Vue、HTML为核心的前端开发技术网站。我们致力于为广大前端开发者提供专业、全面、实用的前端开发知识和技术支持。 在本网站中,您可以学习到最新的前端开发技术,了解前端开发的最新趋势和最佳实践。我们提供丰富的教程和案例,让您可以快速掌握前端开发的核心技术和流程。 本网站还提供一系列实用的工具和插件,帮助您更加高效地进行前端开发工作。我们提供的工具和插件都经过精心设计和优化,可以帮助您节省时间和精力,提升开发效率。 除此之外,本网站还拥有一个活跃的社区,您可以在社区中与其他前端开发者交流技术、分享经验、解决问题。我们相信,社区的力量可以帮助您更好地成长和进步。 在本网站中,您可以找到您需要的一切前端开发资源,让您成为一名更加优秀的前端开发者。欢迎您加入我们的大家庭,一起探索前端开发的无限可能!