当前位置: 首页 > 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明天再水,指标要完成了,这个月已经超常发挥了哈!

相关文章:

  • 格雷希尔G80L-T系列大口径快速连接器,在汽车膨胀水箱的气密性测试密封方案
  • JavaScript入门--数组
  • 基于flutter3.x+window_manager+getx桌面端仿macOS系统
  • 提升数据质量的三大要素:清洗prompt、数据溯源、数据增强(含Reviewer2和PeerRead)​
  • Unity HDRP 2022 Release-Notes
  • Transformer架构实现一
  • Self-attention与Word2Vec
  • OpenHarmony Docker移植实践
  • openEuler22.03-LTS-SP1离线升级openEuler22.03-LTS-SP3
  • 【论文解读】transformer小目标检测综述
  • 【计算机网络】DNS/ICMP协议/NAT技术
  • Spring Boot基础面试问题(一)
  • 软件测试过程:单元测试,集成测试,系统测试,验收测试,回归测试
  • 入门力扣自学笔记177 C++ (题目编号:769)
  • QT中QThread的各个方法,UI线程关系,事件关系详解(5)
  • 深度学习:LeNet-5实现服装分类(PyTorch)
  • 核酸系统架构设计
  • 基于Matlab模拟、检测和跟踪飞机着陆进场中异常的仿真(附源码)
  • 面试官:服务端推送到Web前端有哪几种方式?
  • selenium使用篇_键盘鼠标事件
  • Java第17章 - I/O流(一)
  • STM32F103移植FreeRTOS必须搞明白的系列知识---4(FreeRTOSConfig.h配置文件)
  • C语言中的字符串转数字函数常见问题详解
  • 从零开始搭建仿抖音短视频APP-构建后端项目
  • 力扣 221. 最大正方形
  • 爬虫报错:twisted.web._newclient.ResponseNeverReceived
  • 前后端分离技术渲染动态页面 - 表白墙(Servlet)
  • 关于springboot多环境设设置说明
  • 一幅长文细学CSS3
  • P2-Net:用于2D像素与3D点匹配的局部特征的联合描述符和检测器(ICCV 2021)
  • 电磁兼容(EMC)基础(二)
  • 面向终客户和设备制造商的Ethernet-APL