当前位置: 首页 > news >正文

Java反射小练之手写BeanUtils的copyProperties(Upgrade)

文章目录

  • 前言
    • 问题解决思路
    • 实际解决方案
  • 转换实现
  • 升级
    • 基本功能
    • 编码
    • 完整实现
  • 总结

前言

在一个风和日丽的下午,开起来一天的代码之旅,结果发现了一个bug,没错事情是这样子的:
有这样一段代码:
在这里插入图片描述

我们将MybatisPlus的一个QureyWrapper对象存储进去了这个Map里面,这个Map是这样的<String,Object> 所以的话我们是可以将这个对象存进去的。

之后我们还有一段代码需要解析:
在这里插入图片描述

需要将这个名义上的Object对象(实际上,我们拿到的那个accurate_query对象就是Wrapper类型)重新转化回来。

这样看炸一下是木有问题的,因为咱们的其实本来就是这个类型的,只是存储的时候类型放大了,现在放回来。但是不好意思过不了,那么这个时候我就开始想要解决方案了,很显然这个由于类型的问题。解决方案必然是通过反射进行解决,如何通过反射直接进行类型转换(这里提一嘴,我们其实可以去直接相等,写一下逻辑骗过编译器就可以,因为本质上他们就是一个类型的)。

问题解决思路

那么这个时候的话,明确了,我们需要通过反射去进行转换,我们可以去创建一个新的类,但是这个类的属性,对应的值必须是我们原来的。

那么这个时候,我们就要去看一看这个Wrapper里面到底有啥了。
我们进入这个Wrapper
在这里插入图片描述
可以发现里面其实没啥东西,继承了这个抽象的Wrapper,我们进入这个看看。
在这里插入图片描述
在这里的话我们可以发现很多的方法,比如我们常用的追加条件啥的。
在这里插入图片描述
所以这个我们就很容易想到一个解决方案:
那就是在这里插入图片描述
那么这个时候有小伙伴问了,直接替换不就完了,为啥还要QueryWrapper的源码呀。其实很简单,主要是看看有没有特殊的需要注意的地方,验证一下我们能不能通过替换得到我们想要得到的类。

实际解决方案

既然知道了,那么我们就要解决。
解决方案其实很简单,那就是这个:

BeanUtils.copyProperties(accurate_query,blogEntityQueryWrapper);

就这么粗暴。
那我就不服了,我得看看这个玩意咋写的,虽然写得出来。

转换实现

ok,现在就进去看看有啥,可以看到源码:

    private static void copyProperties(Object source, Object target, @Nullable Class<?> editable, @Nullable String... ignoreProperties) throws BeansException {
        Assert.notNull(source, "Source must not be null");
        Assert.notNull(target, "Target must not be null");
        Class<?> actualEditable = target.getClass();
        if (editable != null) {
            if (!editable.isInstance(target)) {
                throw new IllegalArgumentException("Target class [" + target.getClass().getName() + "] not assignable to Editable class [" + editable.getName() + "]");
            }

            actualEditable = editable;
        }

        PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);
        List<String> ignoreList = ignoreProperties != null ? Arrays.asList(ignoreProperties) : null;
        PropertyDescriptor[] var7 = targetPds;
        int var8 = targetPds.length;

        for(int var9 = 0; var9 < var8; ++var9) {
            PropertyDescriptor targetPd = var7[var9];
            Method writeMethod = targetPd.getWriteMethod();
            if (writeMethod != null && (ignoreList == null || !ignoreList.contains(targetPd.getName()))) {
                PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName());
                if (sourcePd != null) {
                    Method readMethod = sourcePd.getReadMethod();
                    if (readMethod != null && ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())) {
                        try {
                            if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {
                                readMethod.setAccessible(true);
                            }

                            Object value = readMethod.invoke(source);
                            if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
                                writeMethod.setAccessible(true);
                            }

                            writeMethod.invoke(target, value);
                        } catch (Throwable var15) {
                            throw new FatalBeanException("Could not copy property '" + targetPd.getName() + "' from source to target", var15);
                        }
                    }
                }
            }
        }

    }

通过这段代码我们大概知道了这样几件事情:

1.复制的是属性,而不是字段
2.通过一个目标对象的父类或者其实现的接口来控制需要复制属性的范围

总体的实现也是非常简单,就是通过内省来做的。

和这段代码(初学的时候的代码):

          Class<?> beanClass = Class.forName(prop.getProperty(name));
            bean = beanClass.getConstructor().newInstance();
            Object buyhouse = Class.forName(prop.getProperty("bean.PersonBuyHouse")).getConstructor().newInstance();
            Object houseagent = Class.forName(prop.getProperty("bean.HouseAgent")).getConstructor().newInstance();
            BeanInfo beanInfo = Introspector.getBeanInfo(beanClass);
            PropertyDescriptor[] propertyDescriptor = beanInfo.getPropertyDescriptors();
            for(PropertyDescriptor pd:propertyDescriptor){
                if(pd.getName().equals("houseAgency")){
                    pd.getWriteMethod().invoke(bean, houseagent);
                }else if(pd.getName().equals("buyHouse")){
                    pd.getWriteMethod().invoke(bean, buyhouse);
                }
            }
            

区别就是,人家两个类在换属性。

升级

开玩笑,我彬彬超勇的,现在假设,如果我只是想要改变其中的某一个字段咋办,那这个不行啊,而且它提供的玩意也确实只有这些玩意,那我难受了。那不行,升级,必须升级。

基本功能

首先我们需要实现这些功能

  1. 可以选择性忽略字段
  2. 可以选择性忽略字段
  3. 可以选择性忽略字段

好了,很完美。

编码

ok,现在咱们进行编码。
首先,我们实现一个我们最基本的方法

   public static void copyObjectProperties(Object source,Map<String,Field> sourceFieldMap,Object target,Map<String,Field> targetFieldMap){

        //进行属性值复制
        sourceFieldMap.forEach(
                (fieldName,sourceField) -> {

                    //查看目标对象是否存在这个字段
                    Field targetField = targetFieldMap.get(fieldName);

                    if(targetField != null){

                        try{
                            //对目标字段进行赋值操作
                            targetField.set(target,sourceField.get(source));
                        }catch(IllegalAccessException e){
                            e.printStackTrace();
                        }
                    }
                }
        );
    }

为了实现我上面说的功能,我们这里需要单独存储字段,就是要的那些破玩意。这里是最根本的方法嘛,所以类型是<String,Field>。
我们通过这个方法来解析类:
并且将其存放到缓存

    public static Map<String,Field> getClassFieldMapWithCache(Class<?> sourceClass){

        //查看缓存里面有没有已经解析完毕的现成的数据
        SoftReference<Map<String,Field>> softReference = resolvedClassCache.get(sourceClass);

        //确保classFieldMap的正确初始化和缓存
        if(softReference == null || softReference.get() == null){

            //解析字节码对象
            return resolveClassFieldMap(sourceClass);
        }else {

            //从缓存里面正确的取出数据
            return softReference.get();
        }
    }

通过这个玩意,将完成对类很多属性的操作,例如:

  /**
     * 获取一个对象里面字段不为null的字段名称集合
     */
    public static String[] getNonNullValueFieldNames(Object source){

        //非空校验
        Objects.requireNonNull(source);

        //获取空值字段名称
        String[] nullValueFieldNames = getNullValueFieldNames(source);

        Map<String,Field> classFieldMap = getClassFieldMapWithCache(source.getClass());

        //获取全部的字段名称,因为原数据没办法修改,所以需要重新建立一个集合来进行判断
        Set<String> allFieldNames = new HashSet<>(classFieldMap.keySet());

        //移除掉值为null的字段名称
        allFieldNames.removeAll(Arrays.asList(nullValueFieldNames));

        return allFieldNames.toArray(new String[]{});
    }

那么到这里我们就可以去实现我们要对比字段完成的工作了。

    public static void copyPropertiesWithIgnoreSourceFields(Object source,Object target,String ...ignoreFieldNames){

        Map<String,Field> sourceFieldMap = new HashMap<>(getClassFieldMapWithCache(source.getClass()));

        filterByFieldName(sourceFieldMap,ignoreFieldNames);

        copyObjectProperties(source,sourceFieldMap,target,new HashMap<>(getClassFieldMapWithCache(target.getClass())));
    }

完整实现


import java.lang.ref.SoftReference;
import java.lang.reflect.Field;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

public class ReflectUtils {

    private static final Map<Class<?>,SoftReference<Map<String,Field>>> resolvedClassCache = new ConcurrentHashMap<>();


    /**
     * 获取一个对象里面字段为null的字段名称集合
     */
    public static String[] getNullValueFieldNames(Object source){

        //非空校验:NullPointerException
        Objects.requireNonNull(source);

        Class<?> sourceClass = source.getClass();

        //从缓存里面获取,如果缓存里面没有就会进行第一次反射解析
        Map<String,Field> classFieldMap = getClassFieldMapWithCache(sourceClass);

        List<String> nullValueFieldNames = new ArrayList<>();

        classFieldMap.forEach(
                (fieldName,field) -> {
                    try{
                        //挑选出值为null的字段名称
                        if(field.get(source) == null){
                            nullValueFieldNames.add(fieldName);
                        }
                    }catch(IllegalAccessException e){
                        e.printStackTrace();
                    }
                }
        );
        return nullValueFieldNames.toArray(new String[]{});
    }


    /**
     * 获取一个对象里面字段不为null的字段名称集合
     */
    public static String[] getNonNullValueFieldNames(Object source){

        //非空校验
        Objects.requireNonNull(source);

        //获取空值字段名称
        String[] nullValueFieldNames = getNullValueFieldNames(source);

        Map<String,Field> classFieldMap = getClassFieldMapWithCache(source.getClass());

        //获取全部的字段名称,因为原数据没办法修改,所以需要重新建立一个集合来进行判断
        Set<String> allFieldNames = new HashSet<>(classFieldMap.keySet());

        //移除掉值为null的字段名称
        allFieldNames.removeAll(Arrays.asList(nullValueFieldNames));

        return allFieldNames.toArray(new String[]{});
    }

    
    public static Map<String,Field> resolveClassFieldMap(final Class<?> sourceClass){

        SoftReference<Map<String,Field>> softReference = resolvedClassCache.get(sourceClass);

        //判断是否已经被初始化
        if(softReference == null || softReference.get() == null){

            //对同一个字节码对象的解析是同步的,但是不同字节码对象的解析是并发的
            synchronized(sourceClass){

                softReference = resolvedClassCache.get(sourceClass);

                if(softReference == null || softReference.get() == null){

                    Map<String,Field> fieldMap = new HashMap<>();

                    /*
                    Returns an array of Field objects reflecting all the fields declared by the class or interface represented by this
                    Class object. This includes public, protected, default access, and private fields, but excludes inherited fields
                    */
                    Field[] declaredFields = sourceClass.getDeclaredFields();

                    if(declaredFields != null && declaredFields.length > 0){

                        for(Field field : declaredFields){

                            /*
                            Set the accessible flag for this object to the indicated boolean value.
                            */
                            field.setAccessible(true);

                            fieldMap.put(field.getName(),field);
                        }
                    }

                    //设置为不变Map
                    fieldMap = Collections.unmodifiableMap(fieldMap);

                    softReference = new SoftReference<>(fieldMap);

                    /*
                    更新缓存,将解析后的数据加入到缓存里面去
                     */
                    resolvedClassCache.put(sourceClass,softReference);

                    return fieldMap;
                }
            }
        }

        /*
        运行到这里来的时候要么早就存在,要么就是已经被其他的线程给初始化了
         */
        return softReference.get();
    }

    public static Map<String,Field> getClassFieldMapWithCache(Class<?> sourceClass){

        //查看缓存里面有没有已经解析完毕的现成的数据
        SoftReference<Map<String,Field>> softReference = resolvedClassCache.get(sourceClass);

        //确保classFieldMap的正确初始化和缓存
        if(softReference == null || softReference.get() == null){

            //解析字节码对象
            return resolveClassFieldMap(sourceClass);
        }else {

            //从缓存里面正确的取出数据
            return softReference.get();
        }
    }

    public static void copyObjectProperties(Object source, Map<String, Field> sourceFieldMap, Object target, Map<String,Field> targetFieldMap){

        //进行属性值复制
        sourceFieldMap.forEach(
                (fieldName,sourceField) -> {

                    //查看目标对象是否存在这个字段
                    Field targetField = targetFieldMap.get(fieldName);

                    if(targetField != null){

                        try{
                            //对目标字段进行赋值操作
                            targetField.set(target,sourceField.get(source));
                        }catch(IllegalAccessException e){
                            e.printStackTrace();
                        }
                    }
                }
        );
    }
    public static void filterByFieldName(Map<String,Field> fieldMap,String ... ignoreFieldNames){

        //需要忽略的对象字段
        List<String> ignoreNames = ReflectUtil.<String>resolveArrayToList(ignoreFieldNames);

        //移除忽略的对象字段
        fieldMap.keySet().removeAll(ignoreNames);
    }

    /**
     * 将一个对象里面字段相同、类型兼容的数据复制到另外一个对象去
     * 原始功能
     * @param source:从这个对象复制
     * @param target:复制到这个对象来
     */
    public static void copyPropertiesSimple(Object source,Object target){

        copyObjectProperties(
                source,new HashMap<>(getClassFieldMapWithCache(source.getClass())),
                target,new HashMap<>(getClassFieldMapWithCache(target.getClass())));
    }
    /**
     * 忽略掉非空的字段或者空的字段
     */
    public static void filterByFieldValue(Object object,Map<String,Field> fieldMap,boolean filterNullAble){

        Iterator<String> iterator = fieldMap.keySet().iterator();

        if(filterNullAble){

            while(iterator.hasNext()){

                try{
                    //移除值为null的字段
                    if(fieldMap.get(iterator.next()).get(object) == null){
                        iterator.remove();
                    }
                }catch(IllegalAccessException e){
                    e.printStackTrace();
                }
            }
        }else {

            while(iterator.hasNext()){

                try{
                    //移除字段不为null的字段
                    if(fieldMap.get(iterator.next()).get(object) != null){
                        iterator.remove();
                    }
                }catch(IllegalAccessException e){
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 忽略掉原对象字段值为null的字段
     */
    public static void copyPropertiesWithNonNullSourceFields(Object source,Object target){

        Map<String,Field> sourceFieldMap = new HashMap<>(getClassFieldMapWithCache(source.getClass()));

        filterByFieldValue(source,sourceFieldMap,true);

        copyObjectProperties(source,sourceFieldMap,target,new HashMap<>(getClassFieldMapWithCache(target.getClass())));
    }

    /**
     * 忽略掉原对象的指定字段的复制
     * @param ignoreFieldNames:需要忽略的原对象字段名称集合
     */
    public static void copyPropertiesWithIgnoreSourceFields(Object source,Object target,String ...ignoreFieldNames){

        Map<String,Field> sourceFieldMap = new HashMap<>(getClassFieldMapWithCache(source.getClass()));

        filterByFieldName(sourceFieldMap,ignoreFieldNames);

        copyObjectProperties(source,sourceFieldMap,target,new HashMap<>(getClassFieldMapWithCache(target.getClass())));
    }

    /**
     * 忽略掉目标对象的指定字段的复制
     * @param ignoreFieldNames:需要忽略的原对象字段名称集合
     */
    public static void copyPropertiesWithIgnoreTargetFields(Object source,Object target,String ...ignoreFieldNames){

        Map<String,Field> targetFieldMap = new HashMap<>(getClassFieldMapWithCache(target.getClass()));

        filterByFieldName(targetFieldMap,ignoreFieldNames);

        copyObjectProperties(source,new HashMap<>(getClassFieldMapWithCache(source.getClass())),target,targetFieldMap);
    }

    /**
     * 除实现 copyPropertiesSimple 的功能外,如果目标对象的属性值不为null将不进行覆盖
     */
    public static void copyPropertiesWithTargetFieldNonOverwrite(Object source,Object target){

        Map<String,Field> targetFieldMap = new HashMap<>(getClassFieldMapWithCache(target.getClass()));

        filterByFieldValue(target,targetFieldMap,false);

        copyObjectProperties(source,new HashMap<>(getClassFieldMapWithCache(source.getClass())),target,targetFieldMap);
    }

}

总结

没什么好总结的,就这样。TSP明天再水,指标要完成了,这个月已经超常发挥了哈!

相关文章:

  • 千粉缔造760w播放!B站“新人”UP主在B站怎么加速上位?
  • 对于B+树,为什么说一般查找行记录,最多只需1~3次磁盘IO
  • 如何在充满不确定性的当下探索未来?
  • FPGA—从加法运算理解流水线的作用
  • i.MX 6ULL 驱动开发 十五:按键中断(input子系统)
  • Django用户认证系统
  • 论坛介绍 | COSCon'22 开源硬件(H)
  • 【Vulnhub靶场】——HARRYPOTTER第三部: FAWKES
  • [附源码]Java计算机毕业设计SSMjava视频点播系统
  • Day768.大佬推荐的经典的Redis学习资料 -Redis 核心技术与实战
  • fastdfs简介及在springboot中使用
  • OpenCASCADE使用(Stp to Gltf)
  • 进程互斥的硬件实现方式【操作系统学习笔记】
  • JavaScript教程-原生的原型,Object.prototype,其他的内建原型,从原型当中借用,原型方法,_proto_
  • (Java岗)秋招打卡!一本学历拿下美团、阿里、快手、米哈游offer
  • JS 中的深拷贝与浅拷贝
  • es的写入过程
  • express + mock 让前后台并行开发
  • happypack两次报错的问题
  • Java 网络编程(2):UDP 的使用
  • jquery cookie
  • linux安装openssl、swoole等扩展的具体步骤
  • rc-form之最单纯情况
  • RedisSerializer之JdkSerializationRedisSerializer分析
  • Redux 中间件分析
  • 利用DataURL技术在网页上显示图片
  • 面试题:给你个id,去拿到name,多叉树遍历
  • 那些被忽略的 JavaScript 数组方法细节
  • 分布式关系型数据库服务 DRDS 支持显示的 Prepare 及逻辑库锁功能等多项能力 ...
  • (C++)栈的链式存储结构(出栈、入栈、判空、遍历、销毁)(数据结构与算法)
  • (差分)胡桃爱原石
  • (翻译)Entity Framework技巧系列之七 - Tip 26 – 28
  • (转)甲方乙方——赵民谈找工作
  • *Django中的Ajax 纯js的书写样式1
  • .apk文件,IIS不支持下载解决
  • .NET CF命令行调试器MDbg入门(三) 进程控制
  • .NET core 自定义过滤器 Filter 实现webapi RestFul 统一接口数据返回格式
  • .NET Framework杂记
  • .netcore如何运行环境安装到Linux服务器
  • .Net程序帮助文档制作
  • .NET设计模式(11):组合模式(Composite Pattern)
  • ??eclipse的安装配置问题!??
  • @RestControllerAdvice异常统一处理类失效原因
  • [Android Pro] android 混淆文件project.properties和proguard-project.txt
  • [BUUCTF]-Reverse:reverse3解析
  • [BZOJ] 1001: [BeiJing2006]狼抓兔子
  • [codeforces]Recover the String
  • [c语言]小课堂 day2
  • [Grafana]ES数据源Alert告警发送
  • [HackMyVM]靶场 Wild
  • [LeetCode]-283. 移动零-1089. 复写零
  • [Linux] Boot分区满了的处理方法 The volume boot has only 0 bytes disk space remaining
  • [Linux]进程间通信(system V共享内存 | system V信号量)
  • [Linux]知识整理(持续更新)
  • [Linux打怪升级之路]-vim编辑器(看就能马上操作噢)