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

Posted 张子行的博客

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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);
                        
                    
                
            
        

    

总结

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

以上是关于Spring提供的BeanUtils源码剖析(附手写copyProperties方法)的主要内容,如果未能解决你的问题,请参考以下文章

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

结合阿里代码规范约定+源码剖析属性拷贝性能安全问题

Spring 之 BeanUtils.copyProperties(...) 源码简读

Spring源码剖析-Autowired自动注入原理

Spring缓存源码剖析:CacheManager

Spring IOC源码剖析:Spring IOC容器初始化主体流程