copyProperties源码全解析-理解Introspector(内省)机制

很多项目中都使用了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


        
            org.springframework
            spring-beans
            5.2.13.RELEASE
        
        
            commons-beanutils
            commons-beanutils
            1.9.3
        
    
方法调用测试
  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, 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 
            Map propMap = (Map) orig;
            for (final Map.Entry 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 in) {
        if (fast) {
            synchronized (this) {
                final Map 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 convertToType(final Class 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> 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源码全解析-理解Introspector(内省)机制)