美文网首页
copyProperties源码全解析-理解Introspect

copyProperties源码全解析-理解Introspect

作者: 布朗XD | 来源:发表于2021-03-18 10:38 被阅读0次

    很多项目中都使用了VO、DTO、DO、PO等模型设计,在每一层进行参数传递之后,免不了会进行大量的对象属性之间的拷贝,此时我们会使用到BeanUtils这种工具类,使用copyProperties进行便捷的拷贝,代码如下:

    实体类定义
    package com.brianxia.reflection.instropector;
    
    /**
     * @author brianxia
     * @version 1.0
     * @date 2021/3/17 19:28
     */
    public class Source {
    
        private String name;
        private int age;
    
        public int getAge() {
            return age;
        }
    
        public void setAge(int age) {
            this.age = age;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    }
    
    package com.brianxia.reflection.instropector;
    
    /**
     * @author brianxia
     * @version 1.0
     * @date 2021/3/17 19:28
     */
    public class Target {
    
        private String name;
    
        private long age;
    
        public long getAge() {
            return age;
        }
    
        public void setAge(long age) {
            this.age = age;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        @Override
        public String toString() {
            return "Target{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    '}';
        }
    }
    
    引入相应的依赖

    这里我们选择使用Spring和commons提供的BeanUtil

    <dependencies>
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-beans</artifactId>
                <version>5.2.13.RELEASE</version>
            </dependency>
            <dependency>
                <groupId>commons-beanutils</groupId>
                <artifactId>commons-beanutils</artifactId>
                <version>1.9.3</version>
            </dependency>
        </dependencies>
    
    方法调用测试
      Source source = new Source();
      source.setName("张三");
      source.setAge(18);
      Target target = new Target();
      //Spring
      BeanUtils.copyProperties(source,target);
      //commons
      BeanUtils.copyProperties(target,source);
    

    源码剖析

    • Spring实现


      image.png

      这是Spring的方法定义,这里有四个参数,Spring对于复制属性做了一些额外的处理:

    source – 源bean
    target – 目标bean
    editable – 可以限制只拷贝父类或者接口中定义的属性
    ignoreProperties – 可变参数列表,排除某些特定的属性
    

    我们来看下Spring是如何实现的:

    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;
            }
    

    如果传递了editable参数,那么就以editable的Class为准,获取属性。

    PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);
    

    通过内省,获取属性的信息。内省在后文中详细描述。我们详细来看getPropertyDescriptors这个方法:

    image.png
    这里使用了一种CachedIntrospectionResults用来缓存曾经使用过的内省之后的数据,否则每次进行copy都需要重新获取属性信息,性能太低,所以使用进行了缓存优化。CachedIntrospectionResults定义了两种ConcurrentHashMap存放属性信息:
    image.png
    Spring会先从strongClassCache中获取,获取不到再去softClassCache中获取,如果都没有获取到,则进行创建。创建是在CachedIntrospectionResults的构造方法中,其实创建的过程就是将目标类的所有属性的PropertyDescriptor进行了缓存,注意: 如果有父类的话,父类的属性也会缓存起来。然后会将class作为key,将创建的CachedIntrospectionResults作为value,默认缓存到strongClassCache属性中(作为强引用)。源类也一样将PropertyDescriptor缓存到CachedIntrospectionResults中。因此大大提升了性能。
    static CachedIntrospectionResults forClass(Class<?> beanClass) throws BeansException {
            CachedIntrospectionResults results = strongClassCache.get(beanClass);
            if (results != null) {
                return results;
            }
            results = softClassCache.get(beanClass);
            if (results != null) {
                return results;
            }
    
            results = new CachedIntrospectionResults(beanClass);
            ConcurrentMap<Class<?>, CachedIntrospectionResults> classCacheToUse;
    
            if (ClassUtils.isCacheSafe(beanClass, CachedIntrospectionResults.class.getClassLoader()) ||
                    isClassLoaderAccepted(beanClass.getClassLoader())) {
                classCacheToUse = strongClassCache;
            }
            else {
                if (logger.isDebugEnabled()) {
                    logger.debug("Not strongly caching class [" + beanClass.getName() + "] because it is not cache-safe");
                }
                classCacheToUse = softClassCache;
            }
    
            CachedIntrospectionResults existing = classCacheToUse.putIfAbsent(beanClass, results);
            return (existing != null ? existing : results);
        }
    

    这里有一个方法需要注意:ClassUtils.isCacheSafe,这个方法会检查给定的beanClass是否由给定的classloader或者此classloader的祖先加载的(双亲委派的原理)。
    所以第一次加载同一个类的属性会比较慢,后续使用缓存就不用重复加载了。
    回到拷贝代码部分,接下来就是比较常规的使用属性信息获取Read和Write方法,其实也就是获取setter和getter方法,使用反射进行调用:

            for (PropertyDescriptor targetPd : targetPds) {
                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 ex) {
                                throw new FatalBeanException(
                                        "Could not copy property '" + targetPd.getName() + "' from source to target", ex);
                            }
                        }
                    }
                }
            }
    

    这里Spring对于非public的方法进行了setAccessible处理,使之有处理权限。但是Spring比较大的问题是装箱类型LongInteger等互相转换无法做到:

    ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())
    

    这个方法会判断set和get的对象是否有继承关系,如果没有继承关系,就直接返回。而方法内部只是简简单单处理了基本数据类型和装箱类型的关系,并未对数据进行特殊的处理。

     public static boolean isAssignable(Class<?> lhsType, Class<?> rhsType) {
            Assert.notNull(lhsType, "Left-hand side type must not be null");
            Assert.notNull(rhsType, "Right-hand side type must not be null");
            if (lhsType.isAssignableFrom(rhsType)) {
                return true;
            } else {
                Class resolvedWrapper;
                if (lhsType.isPrimitive()) {
                    resolvedWrapper = (Class)primitiveWrapperTypeMap.get(rhsType);
                    return lhsType == resolvedWrapper;
                } else {
                    resolvedWrapper = (Class)primitiveTypeToWrapperMap.get(rhsType);
                    return resolvedWrapper != null && lhsType.isAssignableFrom(resolvedWrapper);
                }
            }
        }
    

    整体来说,Spring对于copyProperties的实现过于简单,仅仅增加了ignore忽略和editable限制父类拷贝等基础功能,但未对复杂的数据类型转换做出特殊处理。接下来我们来看commons的实现。

    • commons实现
      从整体结构上来说,commons的实现做了很多的特殊处理提升性能。


      image.png

      它将数据类型分成了三种,DynaBean(姑且称之为万能Bean,自行查询下资料)、Map、JavaBean。
      DynaBean使用较少,我们先说Map的实现:

      @SuppressWarnings("unchecked")
                final
                // Map properties are always of type <String, Object>
                Map<String, Object> propMap = (Map<String, Object>) orig;
                for (final Map.Entry<String, Object> entry : propMap.entrySet()) {
                    final String name = entry.getKey();
                    if (getPropertyUtils().isWriteable(dest, name)) {
                        copyProperty(dest, name, entry.getValue());
                    }
                }
    

    直接从Map中遍历Entry,然后将Entry的内容写入到目标对象中。而JavaBean的处理如下:

                final PropertyDescriptor[] origDescriptors =
                    getPropertyUtils().getPropertyDescriptors(orig);
                for (PropertyDescriptor origDescriptor : origDescriptors) {
                    final String name = origDescriptor.getName();
                    if ("class".equals(name)) {
                        continue; // No point in trying to set an object's class
                    }
                    if (getPropertyUtils().isReadable(orig, name) &&
                        getPropertyUtils().isWriteable(dest, name)) {
                        try {
                            final Object value =
                                getPropertyUtils().getSimpleProperty(orig, name);
                            copyProperty(dest, name, value);
                        } catch (final NoSuchMethodException e) {
                            // Should not happen
                        }
                    }
                }
    

    与Spring类似,内部维护了两个WeakFastHashMap缓存属性信息,WeakFastHashMap的设计很巧妙,借鉴了CopyOnWrite的思想,查询数据时无需加锁,它会选择clone一份新的数据进行修改,在clone出来的数据上进行修改,然后再替换原来的数据。比如如下代码:

    
     @Override
        public V get(final Object key) {
            if (fast) {
                return (map.get(key));
            } else {
                synchronized (map) {
                    return (map.get(key));
                }
            }
        }
    
     @Override
        public void putAll(final Map<? extends K, ? extends V> in) {
            if (fast) {
                synchronized (this) {
                    final Map<K, V> temp =  cloneMap(map);
                    temp.putAll(in);
                    map = temp;
                }
            } else {
                synchronized (map) {
                    map.putAll(in);
                }
            }
        }
    

    commons对于数据类型转换,有专门的函数convertForCopy来进行处理。

     protected Object convert(final Object value, final Class<?> type) {
            final Converter converter = getConvertUtils().lookup(type);
            if (converter != null) {
                log.trace("        USING CONVERTER " + converter);
                return converter.convert(type, value);
            } else {
                return value;
            }
        }
    

    首先根据数据类型找到对应的converter,比如IntegerConverter[UseDefault=true, UseLocaleFormat=false],使用装饰者模式进行增强。根据具体转换出的类型,使用Number进行处理。

        @Override
        protected <T> T convertToType(final Class<T> targetType, final Object value) throws Throwable {
    
            final Class<?> sourceType = value.getClass();
            // Handle Number
            if (value instanceof Number) {
                return toNumber(sourceType, targetType, (Number)value);
            }
    
            // Handle Boolean
            if (value instanceof Boolean) {
                return toNumber(sourceType, targetType, ((Boolean)value).booleanValue() ? ONE : ZERO);
            }
    
            // Handle Date --> Long
            if (value instanceof Date && Long.class.equals(targetType)) {
                return targetType.cast(new Long(((Date)value).getTime()));
            }
    
            // Handle Calendar --> Long
            if (value instanceof Calendar  && Long.class.equals(targetType)) {
                return targetType.cast(new Long(((Calendar)value).getTime().getTime()));
            }
    
            // Convert all other types to String & handle
            final String stringValue = value.toString().trim();
            if (stringValue.length() == 0) {
                return handleMissing(targetType);
            }
    
            // Convert/Parse a String
            Number number = null;
            if (useLocaleFormat) {
                final NumberFormat format = getFormat();
                number = parse(sourceType, targetType, stringValue, format);
            } else {
                if (log().isDebugEnabled()) {
                    log().debug("    No NumberFormat, using default conversion");
                }
                number = toNumber(sourceType, targetType, stringValue);
            }
    
            // Ensure the correct number type is returned
            return toNumber(sourceType, targetType, number);
    
        }
    

    比如此例中Long转换成Integer类型,只需要调用toNumber即可。针对Long型进行特殊的处理。

       // Long
            if (targetType.equals(Long.class)) {
                return targetType.cast(new Long(value.longValue()));
            }
    

    总结:commons的实现要比Spring功能更加强大,不仅使用了具备COW技术的缓存大大增强并发读取能力,同时对数据转换做了严格的处理。

    内省

    最后我们来说一下Java中的内省(Introspector)机制。Introspector与反射类似,主要是对Java Bean属性、方法等的一种处理方法。

    • PropertyDescriptor类:
      PropertyDescriptor类表示JavaBean类通过存储器导出一个属性。主要方法:
        1. getPropertyType(),获得属性的Class对象;
        2. getReadMethod(),获得用于读取属性值的方法;getWriteMethod(),获得用于写入属性值的方法;
        3. hashCode(),获取对象的哈希值;
        4. setReadMethod(Method readMethod),设置用于读取属性值的方法;
        5. setWriteMethod(Method writeMethod),设置用于写入属性值的方法。

    • Introspector类:

    将JavaBean中的属性封装起来进行操作。在程序把一个类当做JavaBean来看,就是调用Introspector.getBeanInfo()方法,得到的BeanInfo对象封装了把这个类当做JavaBean看的结果信息,即属性的信息。

    getPropertyDescriptors(),获得属性的描述,可以采用遍历BeanInfo的方法,来查找、设置类的属性。

    手写一个copyProperties

    package com.brianxia.reflection.instropector;
    
    import org.springframework.beans.BeanUtils;
    
    import javax.xml.ws.spi.Invoker;
    import java.beans.BeanInfo;
    import java.beans.IntrospectionException;
    import java.beans.Introspector;
    import java.beans.PropertyDescriptor;
    import java.lang.reflect.InvocationTargetException;
    import java.lang.reflect.Method;
    import java.lang.reflect.Modifier;
    import java.util.HashMap;
    import java.util.Map;
    import java.util.concurrent.ConcurrentHashMap;
    
    /**
     * @author brianxia
     * @version 1.0
     * @date 2021/3/17 19:22
     */
    public class InstropectorDemo {
    
        public static Map<Class,Map<String,PropertyDescriptor>> sourcePd = new ConcurrentHashMap<>();
    
        public synchronized static void createPd(Object source) throws IntrospectionException {
            Class clazz = source.getClass();
            if(!sourcePd.containsKey(clazz)){
                sourcePd.put(clazz,new HashMap<>());
            }
            Map putData = sourcePd.get(clazz);
            //获取BeanInfo
            BeanInfo beanInfo = Introspector.getBeanInfo(source.getClass());
            //获取属性描述信息
            PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
    
            for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
                putData.put(propertyDescriptor.getName(),propertyDescriptor);
            }
        }
    
        public static PropertyDescriptor getPd(String name,Class clazz) {
            if(!sourcePd.containsKey(clazz)){
                return null;
            }
    
            return sourcePd.get(clazz).get(name);
        }
    
        /**
         *
         * @param source  the source bean
         * @param target  the target bean
         */
        public static void copyProperties(Object source,Object target) throws IntrospectionException, InvocationTargetException, IllegalAccessException {
            //获取BeanInfo
            BeanInfo beanInfo = Introspector.getBeanInfo(target.getClass());
            //获取属性描述信息
            PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
    
            Class<?> aClass = source.getClass();
            //创建source的描述信息map
            createPd(source);
    
            //遍历属性描述信息,进行copy
            for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
    
                String name = propertyDescriptor.getName();
                PropertyDescriptor sourcePd = getPd(name,aClass);
                //如果source没有对应属性,直接continue
                if(sourcePd == null){
                    continue;
                }
    
                //获取getter和setter方法
                Method writeMethod = propertyDescriptor.getWriteMethod();
                Method readMethod = sourcePd.getReadMethod();
    
                //授予权限 private也可以访问
                if(writeMethod != null && readMethod != null){
                    if(!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())){
                        writeMethod.setAccessible(true);
                    }
    
                    if(!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())){
                        readMethod.setAccessible(true);
                    }
    
                    //复制属性
                    Object invoke = readMethod.invoke(source);
                    writeMethod.invoke(target,invoke);
                }
            }
        }
    
        public static void main(String[] args) throws IntrospectionException, InvocationTargetException, IllegalAccessException {
            Source source = new Source();
            source.setName("张三");
            source.setAge(18L);
            Target target = new Target();
    
            long start = System.currentTimeMillis();
            for (int i = 0; i < 1000000; i++) {
                //target.setAge(source.getAge());
               // target.setName(source.getName());
                copyProperties(source,target);
                //BeanUtils.copyProperties(source,target);
                //org.apache.commons.beanutils.BeanUtils.copyProperties(target,source);
            }
            long end = System.currentTimeMillis();
            System.out.println(end - start);
            System.out.println(target);
    
        }
    }
    
    

    此案例中类型转换并未特别处理,大家可以根据commons的实现自行处理简单的转换。

    相关文章

      网友评论

          本文标题:copyProperties源码全解析-理解Introspect

          本文链接:https://www.haomeiwen.com/subject/vvkucltx.html