Spring提供的BeanUtils源码剖析(附手写copyProperties方法)

前言

前段时间刚入职xxx公司,由于公司内部有一套自己的技术架构,并且很多东西都是自研、封装的。身为小菜鸡的在业余时间抽空看了一下里面的源码,感觉这些人基础挺扎实的哈哈哈哈,这些个基础架构工具包全是自研的,牛逼。一下子激发了我看 BeanUtils 源码的兴趣,想着靠自己的摸索,结合之前研究的JDK动态代理技术,自己也尝试着写一套功能类似的基架组件出来(结合代理技术的以后再写,这玩意有点繁琐)。

先导知识之JAVA内省

不清楚JAVA内省的小伙伴,先来了解一下这方面的知识,何为内省?内省是JAVA提供的一组可以操纵 JavaBean 属性的一组 Api,掌握了这些个 Api 我们也可以写一个自己的 BeanUtils 出来。下面先来介绍一波这些个 Api。

  1. Introspector.getBeanInfo(o.getClass()) 获取当类的一个BeanInfo对象,和Spring中的 BeanDefinition 对象有点类似,BeanDefinition 是封装了 Bean 信息的一个信息对象,BeanInfo 是封装了 Java 对象一系列 Api 方法的对象。
  2. BeanInfo.getPropertyDescriptors() 获取 PropertyDescriptor 对象,可以通过此对象获取类字段属性 name、value、get和set方法名字、对字段进行属性填充。等操作

山寨版 BeanUtils.copyProperties()

恭喜你已经通过了入职考核,有资格进入 zzh 盗版集团来,我们集团这里的人都是人中翘楚、人中龙凤、国之栋梁、梁之中流砥柱、柱中之无懈可击的存在了。下面列举一个 我司开发的山寨版BeanUtils.copyProperties()软件 给读者参观(),温馨提示:前方500米有宝箱,请大家仔细翻阅。大概流程为:遍历操作对源对象与目标对象进行属性名的匹配,匹配上了,随即进行对应属性名的value值填充

public class IntrospectionTest {
    public static List<Object> init() {
        List<Object> zzhSources = new ArrayList<>();
        ZzhSource zzhSource = new ZzhSource();
        zzhSource.setAge("1");
        zzhSource.setName("zzh1");
        ZzhSource zzhSource2 = new ZzhSource();
        zzhSource2.setAge("2");
        zzhSource2.setName("zzh2");
        zzhSources.add(zzhSource);
        zzhSources.add(zzhSource2);
        return zzhSources;
    }

    @SneakyThrows
    public static void main(String[] args) {
        List<Object> zzhTargets = new ArrayList<>();
        System.err.println("copy before: " + zzhTargets);
        copyProperties(init(), zzhTargets, ZzhTarget.class);
        System.err.println("copy after:: " + zzhTargets);
    }

    public static void copyProperties(List<Object> sources, List<Object> targets, Class targetClass) {
        sources.stream().forEach(source -> {
            try {
                Object o = targetClass.newInstance();
                Arrays.stream(Introspector.getBeanInfo(source.getClass()).getPropertyDescriptors()).forEach(sp -> {
                    try {
                        /**
                         * 遍历操作:targetClass 对应的对象与 sources 集合中对应的对象进行属性匹配
                         */
                        Arrays.stream(Introspector.getBeanInfo(o.getClass()).getPropertyDescriptors()).forEach(op -> {
                            /**
                             * 名称匹配上了之后进行属性填充
                             */
                            if (sp.getName().equals(op.getName()) && !sp.getName().equals("class")) {
                                try {
                                    /**
                                     * 对我们新创建的 o 对象进行属性填充,填充数据的来源为 sources
                                     */
                                    op.getWriteMethod().invoke(o, sp.getReadMethod().invoke(source));
                                } catch (IllegalAccessException e) {
                                    e.printStackTrace();
                                } catch (InvocationTargetException e) {
                                    e.printStackTrace();
                                }
                            }
                        });
                    } catch (IntrospectionException e) {
                        e.printStackTrace();
                    }
                });
                /**
                 * o 对象属性填充完毕,添加至 targets集合中
                 */
                targets.add(o);
            } catch (IntrospectionException e) {
                e.printStackTrace();
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        });
    }

    @Data
    static class ZzhSource {
        String name;
        String age;
        String sex;
    }

    @Data
    static class ZzhTarget {
        String name;
        String age;
        String sex;
    }
}

效果展示

可以看到数据成功的拷贝到了我们指定的 List 中了
在这里插入图片描述

正版 BeanUtils.copyProperties() 源码剖析

步骤流程总是惊人的相似,获取对象的 PropertyDescriptor ,然后调用内省为我们提供的各种操作 字段 属性的Api,最终达到属性拷贝的目的。

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
        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
        PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);
        List<String> ignoreList = ignoreProperties != null ? Arrays.asList(ignoreProperties) : null;
        PropertyDescriptor[] var7 = targetPds;
        int var8 = targetPds.length;
//遍历目标对象的 PropertyDescriptor
        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
                PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName());
                if (sourcePd != null) {
                //获取源对象的读方法,通过此方法可以获取对应字段的 value值
                    Method readMethod = sourcePd.getReadMethod();
                    if (readMethod != null && ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())) {
                        try {
                        //设置一下方法操作权限,为可操作
                            if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {
                                readMethod.setAccessible(true);
                            }
//获取源对象字段中的 value值
                            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);
                        }
                    }
                }
            }
        }

    }

上班一年的时候补充:Map转对象(补充)

最近在写 Es 相关的业务,但是懒得用框架,主要是 Es 6 和 Es 7 和 Es 8 之间的 api 语法相差过大,实在是垃圾,于是自己基于原始 Api 封装了一套基于组内同时使用,其中就包括 Map 转 对象,传统写法需要一个个 Set 实在是垃圾,于是用反射封转了如下一个抽象方法。

public interface Trans<T> {
    abstract Class getTargetClass();

    default Object trans(Map<String, Object> map) throws IntrospectionException, InstantiationException, IllegalAccessException {
        Object o = getTargetClass().newInstance();
        BeanInfo beanInfo = Introspector.getBeanInfo(o.getClass());
        Arrays.stream(beanInfo.getPropertyDescriptors()).forEach(propertyDescriptor -> {
            if (!"this".equals(propertyDescriptor.getName())&&!Objects.isNull(map.get(propertyDescriptor.getName()))) {
                try {
                    propertyDescriptor.getWriteMethod().invoke(o, map.get(propertyDescriptor.getName()));
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                } catch (InvocationTargetException e) {
                    e.printStackTrace();
                }
            }
        });
        return o;
    }
}

抽象接口 Trans 使用

使用很简单每个业务对象实现 Trans 中的抽象方法即可

在这里插入图片描述

后续 Es 查到数据直接 new ArticleResponse().trans(hit.getSourceAsMap()) 即可得到填充好的对象,简单方便!

在这里插入图片描述

初版工具类的 Trans 接口存在的 bug优化!加入高亮字段映射

之前没考虑到父类属性赋值的问题,以及 Es 查到的数据源全是字符串,需按照类型进行映射,比如 Es 查到的日期是 2023-12-12 字符串类型的,但是映射实体类是 Date 接受的,这种情况优化了一下代码 如下。
在这里插入图片描述
主要逻辑就是根据字段类型做一个匹配映射,匹配类型对不上的进行强转,以及加入高亮字段替换!!!

/**
 * map 转对象
 * author:zzh
 *
 * @param <T>
 */
public interface Trans<T> {
    SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    abstract Class getTargetClass();

    default PropertyDescriptor[] mergeAndDeduplicate(PropertyDescriptor[]... arrays) {
        Set<PropertyDescriptor> set = new HashSet<>();
        for (PropertyDescriptor[] array : arrays) {
            set.addAll(Arrays.asList(array));
        }
        return set.toArray(new PropertyDescriptor[0]);
    }

    default Object trans(Map<String, Object> map, Map<String, HighlightField> highlightFields) throws IntrospectionException, InstantiationException, IllegalAccessException {
        Object o = getTargetClass().newInstance();
        Class tclass = getTargetClass();
        HashMap<String, Class> nameTypeMap = new HashMap<>();
        //找到父类的所有字段
        do {
            Arrays.stream(tclass.getDeclaredFields()).forEach(field -> {
                field.setAccessible(true);
                //key:字段名称,value:字段类型
                nameTypeMap.put(field.getName(), field.getType());
            });
            tclass = tclass.getSuperclass();
        } while (!tclass.equals(Object.class));
        PropertyDescriptor[] propertyDescriptors = Introspector.getBeanInfo(o.getClass()).getPropertyDescriptors();
        Arrays.stream(propertyDescriptors).forEach(propertyDescriptor -> {
            if (!"targetClass".equals(propertyDescriptor.getName()) && !Objects.isNull(map.get(propertyDescriptor.getName()))) {
                try {
                    Method writeMethod = propertyDescriptor.getWriteMethod();
                    if (null != writeMethod) {
                        if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
                            writeMethod.setAccessible(true);
                        }
                        Object sourceValue = map.get(propertyDescriptor.getName());
                        Class aClass = nameTypeMap.get(propertyDescriptor.getName());
                        //类型一致,直接赋值
                        if (sourceValue.getClass().equals(aClass)) {
                            writeMethod.invoke(o, highlightFields.get(propertyDescriptor.getName()) != null ?
                                    (highlightFields.get(propertyDescriptor.getName()).getFragments()[0]).toString() :
                                    map.get(propertyDescriptor.getName()));
                        }
                        /**
                         * 类型不一致强转,这里可以搞个策略模式优化优化
                         */
                        else {
                            if (aClass.equals(Date.class)) {
                                Date parse = simpleDateFormat.parse(String.valueOf(map.get(propertyDescriptor.getName())));
                                writeMethod.invoke(o, parse);
                            }
                            if (aClass.equals(Integer.class)) {
                                writeMethod.invoke(o, Integer.valueOf(String.valueOf(map.get(propertyDescriptor.getName()))));
                            }
                            if (aClass.equals(Long.class)) {
                                writeMethod.invoke(o, Long.valueOf(String.valueOf(map.get(propertyDescriptor.getName()))));
                            }
                            if (aClass.equals(int.class)) {
                                writeMethod.invoke(o, Integer.parseInt(String.valueOf(map.get(propertyDescriptor.getName()))));
                            }
                        }
                    } else throw new RuntimeException(propertyDescriptor.getName() + "~ writeMethod is null!!!!!");
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                } catch (InvocationTargetException e) {
                    e.printStackTrace();
                } catch (Exception e) {
                    //类型不匹配
//                    System.err.println(propertyDescriptor.getWriteMethod());
//                    System.err.println(propertyDescriptor.getName());
//                    System.err.println(o);
//                    System.err.println(map.get(propertyDescriptor.getName()));
                    e.printStackTrace();
                }
            }
        });
        return o;
    }
}

抽象接口 Trans 优化

又优化了一下复杂对象下高亮字段的映射!!!!!最终代码如下

/**
 * map 转对象
 * author:zzh
 *
 * @param <T>
 */
public interface Trans<T> {
    SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    abstract Class getTargetClass();

    default PropertyDescriptor[] mergeAndDeduplicate(PropertyDescriptor[]... arrays) {
        Set<PropertyDescriptor> set = new HashSet<>();
        for (PropertyDescriptor[] array : arrays) {
            set.addAll(Arrays.asList(array));
        }
        return set.toArray(new PropertyDescriptor[0]);
    }

    default Object trans(Map<String, Object> map, Map<String, HighlightField> highlightFieldsSource, List<String> highLightFields) throws IntrospectionException, InstantiationException, IllegalAccessException {
        Object o = getTargetClass().newInstance();
        Class tclass = getTargetClass();
        HashMap<String, Class> nameTypeMap = new HashMap<>();
        //找到父类的所有字段
        do {
            Arrays.stream(tclass.getDeclaredFields()).forEach(field -> {
                field.setAccessible(true);
                //key:字段名称,value:字段类型
                nameTypeMap.put(field.getName(), field.getType());
            });
            tclass = tclass.getSuperclass();
        } while (!tclass.equals(Object.class));
        PropertyDescriptor[] propertyDescriptors = Introspector.getBeanInfo(o.getClass()).getPropertyDescriptors();
        Arrays.stream(propertyDescriptors).forEach(propertyDescriptor -> {
            if (!"targetClass".equals(propertyDescriptor.getName()) && !Objects.isNull(map.get(propertyDescriptor.getName()))) {
                try {
                    Method writeMethod = propertyDescriptor.getWriteMethod();
                    if (null != writeMethod) {
                        if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
                            writeMethod.setAccessible(true);
                        }
                        Object sourceValue = map.get(propertyDescriptor.getName());
                        //父类以及自己所有字段类型
                        Class aClass = nameTypeMap.get(propertyDescriptor.getName());
                        //字符串类型以及高亮直接赋值
                        if (sourceValue.getClass().equals(aClass)) {
                            writeMethod.invoke(o, highlightFieldsSource.get(propertyDescriptor.getName()) != null ?
                                    (highlightFieldsSource.get(propertyDescriptor.getName()).getFragments()[0]).toString() :
                                    map.get(propertyDescriptor.getName()));
                        }
                        /**
                         * 类型不一致强转,这里可以搞个策略模式优化优化
                         */
                        else {
                            if (aClass.equals(Date.class)) {
                                Date parse = simpleDateFormat.parse(String.valueOf(map.get(propertyDescriptor.getName())));
                                writeMethod.invoke(o, parse);
                            }
                            if (aClass.equals(Integer.class)) {
                                writeMethod.invoke(o, Integer.valueOf(String.valueOf(map.get(propertyDescriptor.getName()))));
                            }
                            if (aClass.equals(Long.class)) {
                                writeMethod.invoke(o, Long.valueOf(String.valueOf(map.get(propertyDescriptor.getName()))));
                            }
                            if (aClass.equals(List.class)) {
                                ArrayList<Map<String, Object>> oraginSources = (ArrayList<Map<String, Object>>) map.get(propertyDescriptor.getName());
                                if (null != oraginSources &&
                                        0 != highlightFieldsSource.size()) {
                                    for (int i = 0; i < oraginSources.size(); i++) {
                                        for (int j = 0; j < highLightFields.size(); j++) {
                                            try {
                                                if (highlightFieldsSource.containsKey(highLightFields.get(j))) {
                                                    oraginSources.get(i).put(highLightFields.get(j).split("\\.")[1],
                                                            highlightFieldsSource.get(highLightFields.get(j)).getFragments()[j].toString());
                                                }
                                            } catch (Exception e) {
                                                e.printStackTrace();
                                            }
                                        }
                                    }
                                }
                                writeMethod.invoke(o, oraginSources);
                            }
                            if (aClass.equals(int.class)) {
                                writeMethod.invoke(o, Integer.parseInt(String.valueOf(map.get(propertyDescriptor.getName()))));
                            }
                        }
                    } else throw new RuntimeException(propertyDescriptor.getName() + "~ writeMethod is null!!!!!");
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                } catch (InvocationTargetException e) {
                    e.printStackTrace();
                } catch (Exception e) {
                    //类型不匹配
//                    System.err.println(propertyDescriptor.getWriteMethod());
//                    System.err.println(propertyDescriptor.getName());
//                    System.err.println(o);
//                    System.err.println(map.get(propertyDescriptor.getName()));
                    e.printStackTrace();
                }
            }
        });
        return o;
    }

}

抽象接口 Trans 最终效果

使用方法如下
在这里插入图片描述
可以看到复杂对象也成功加上高亮标签
在这里插入图片描述

总结

Spring为我们提供的 BeanUtils 工具类其实也就是对 内省 Api中的一些方法的包转而已,思考:内省的实现原理是什么呢?我们是否可以从 Jdk 或者 CgLib动态代理入手,自己写一套 山寨版 内省 Api呢?答案肯定是可以的,以后有时间再去研究一下这个,读者感兴趣的话,也可以尝试着自己写一套内省 Api哦

小咸鱼的技术窝

关注不迷路,日后分享更多技术干货,B站、CSDN、微信公众号同名,名称都是(小咸鱼的技术窝)更多详情在主页
在这里插入图片描述

  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小咸鱼的技术窝

你的鼓励将是我最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值