BeanUtils对象属性copy的性能对比以及源码分析

1. 对象属性拷贝的常见方式及其性能

在日常编码中,经常会遇到DO、DTO对象之间的转换,如果对象本身的属性比较少的时候,那么我们采用硬编码手工setter也还ok,但如果对象的属性比较多的情况下,手工setter就显得又low又效率又低。这个时候我们就考虑采用一些工具类来进行对象属性的拷贝了。

我们常用的对象属性拷贝的方式有:

  • Hard Code
  • net.sf.cglib.beans.BeanCopier#copy
  • org.springframework.beans.BeanUtils.copyProperties
  • org.apache.commons.beanutils.PropertyUtils.copyProperties
  • org.apache.commons.beanutils.BeanUtils.copyProperties

针对以上的拷贝方式,我做了一个简单的性能测试,结果如下:

拷贝方式 对象数量: 1 对象数量: 1000 对象数量: 100000 对象数量: 1000000
Hard Code 0 ms 1 ms 18 ms 43 ms
cglib.BeanCopier 111 ms 117 ms 107 ms 110 ms
spring.BeanUtils 116 ms 137 ms 246 ms 895 ms
apache.PropertyUtils 167 ms 212 ms 601 ms 7869 ms
apache.BeanUtils 167 ms 275 ms 1732 ms 12380 ms

测试环境:OS=macOS 10.14, CPU=2.5 GHz,Intel Core I7, Memory=16 GB, 2133MHz LPDDR3

测试方法:通过copy指定数量的复杂对象,分别执行每个Case 10次,取其平均值
版本:commons-beanutils:commons-beanutils:1.9.3, org.springframework:spring-beans:4.3.5.RELEASE ,cglib:cglib:2.2.2

结论:从测试结果中很明显可以看出采用Hard Code方式进行对象属性Copy性能最佳;采用net.sf.cglib.beans.BeanCopier#copy方式进行对象属性copy性能最稳定;而org.apache.commons.beanutils.BeanUtils.copyProperties 方式在数据量大时性能下降最厉害。所以在日常编程中遇到具有较多属性的对象进行属性复制时优先考虑采用net.sf.cglib.beans.BeanCopier#copy

以上的数据之所产生巨大差距的原因在于其实现原理与方式的不同而导致的,Hard Code直接调用getter & setter方法值,cglib采用的是字节码技术,而后三种均采用反射的方式。前两者性能优异众所周知,但为何同样采用反射的方式进行属性Copy时产生的差异如此巨大呢? 这正是本文我们想要去探究的内容。

我们首先解读org.apache.commons.beanutils.BeanUtils的源码,其次解读org.springframework.beans.BeanUtils源码,最后通过它们各自实现方式来进行论证性能差异

apache.BeanUtilsspring.BeanUtils均采用反射技术实现,也都调用了Java关于反射的高级API——Introspector(内省),因此我们首先要了解Introspector是什么.

2. Introspector

Introspector(内省)是jdk提供的用于描述Java bean支持的属性、方法以及事件的工具;利用此类可得到BeanInfo接口的实现对象,BeanInfo接口中有两个重要的方法:

  • BeanDescriptor getBeanDescriptor();

    BeanDescriptor 提供了java bean的一些全局的信息,如class类型、类名称等

  • PropertyDescriptor[] getPropertyDescriptors()

    PropertyDescriptor 描述了java bean中一个属性并导出了他们的getter & setter方法的SoftReference

Jdk的内省接口极大的简化了反射类信息的方式,通过这组api我们可以很方便进行java bean的反射调用。本组api采用软引用、虚引用来充分利用了空闲的内存;在某些地方(如declaredMethodCache)采用缓存来加速api的执行效率,并且此组api是线程安全的。

使用方式:

BeanInfo beanInfo = Introspector.getBeanInfo(icontext.getTargetClass());
PropertyDescriptor[] descriptors = beanInfo.getPropertyDescriptors();
for(PropertyDescriptor descriptor: descriptors) {
    Method readMethod = descriptor.getReadMethod();
    Method writeMethod = descriptot.getWriteMethod();
    // readMethod.invoke(...);
}

以上就是关于Introspector的简单了解,接下来我们先来看apache.BeanUtils的源码.

3. 源码:apache.BeanUtils

apache.BeanUtils是一个包含了很多静态方法的工具类,而几乎所有的静态方法均是BeanUtilsBean的单例对象提供的实现。BeanUtilsBean是进行JavaBean属性操作的入口方法,它以单实例对外提供功能。但这里有一个不同于普通单例的地方:不同的类加载器拥有不同的实例,每一个类加载器只有一个实例 ,所以这里的单例其实是一个伪单例pseudo-singletion

// ContextClassLoaderLocal对象管理了BeanUtilsBean的所有实例
private static final ContextClassLoaderLocal<BeanUtilsBean>
            BEANS_BY_CLASSLOADER = new ContextClassLoaderLocal<BeanUtilsBean>() {
                        @Override
                        protected BeanUtilsBean initialValue() {
                            return new BeanUtilsBean();
                        }
                    };
public static BeanUtilsBean getInstance() {
    return BEANS_BY_CLASSLOADER.get();
}
// {@link ContextClassLoaderLocal#get}
public synchronized T get() {
    valueByClassLoader.isEmpty();
    try {
        final ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); // 获取当前线程的类加载器
        if (contextClassLoader != null) {
            T value = valueByClassLoader.get(contextClassLoader);
            if ((value == null)
                && !valueByClassLoader.containsKey(contextClassLoader)) {
                value = initialValue(); // 初始化BeanUtilsBean,即 new BeanUtilsBean();
                valueByClassLoader.put(contextClassLoader, value);
            }
            return value;
        }
    } catch (final SecurityException e) { /* SWALLOW - should we log this? */ }
    if (!globalValueInitialized) {
        globalValue = initialValue();
        globalValueInitialized = true;
    }
    return globalValue;
}

当获取到了BeanUtilsBean的实例之后,接下来就是我们进行对象属性拷贝的时候了.

// omit exception
public static void copyProperties(final Object dest, final Object orig){
        BeanUtilsBean.getInstance().copyProperties(dest, orig);
}

copyProperties方法中,针对原始对象的类型分别采用了不同的逻辑:

  • Map : 通过Map的Key与dest中的属性进行匹配,然后赋值;
  • DynaBeanDynaBean顾名思义,它是一种可以形成动态java bean的对象,也就是说它内部会存储属性名称、类型以及对应的值,在copy属性时也是将其内部的属性名称与dest对象的属性名称对应后赋值;
  • 标准Java Bean :这个是我们主要进行分析的类型,它是标准的JavaBean对象;与前两者的差异只是在于对原始bean的取值的处理上.
3.1 针对标准JavaBean进行属性copy时的步骤
public void copyProperties(final Object dest, final Object orig) {
    // omit some code (省略一部分代码) ...
   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
            }
        }
    }
}
  1. 根据原始bean的类型解析、缓存其PropertyDescriptor
  2. 轮询原始bean的每一个PropertyDescriptor ,判断PropertyDescriptor在原始bean中是否可读、在目标bean中是否可写,只有这两个条件都成立时才具备copy的资格
  3. 根据PropertyDescriptor从原始bean中获取对应的值,将值copy至目标bean的对应属性上
3.2 获取Bean的PropertyDescriptor
 final PropertyDescriptor[] origDescriptors =
                getPropertyUtils().getPropertyDescriptors(orig);

获取PropertyDescriptor委托给PropertyUtilsBean对象来实现:

public BeanUtilsBean() {
    this(new ConvertUtilsBean(), new PropertyUtilsBean());
}

PropertyUtilsBean 是用于使用java 反射API来操作Java Bean上getter和setter方法的,此类中的代码原先是位于BeanUtilsBean中的,但是考虑到代码量的原因进行了分离(Much of this code was originally included in BeanUtils, but has been separated because of the volume of code involved)。

PropertyUtilsBean中,每个Bean的PropertyDescriptor 会存储于BeanIntrospectionData对象中,当每次需要获取PropertyDescriptor时,会先从cahche中获取BeanIntrospectionData ;如果不存在,则通过内省API获取BeanIntrospectionData并将其置于缓存中:

private BeanIntrospectionData getIntrospectionData(final Class<?> beanClass) {
    // omit some check code ...
    BeanIntrospectionData data = descriptorsCache.get(beanClass);
    if (data == null) {
        data = fetchIntrospectionData(beanClass);
        descriptorsCache.put(beanClass, data);
    }
    return data;
}

private BeanIntrospectionData fetchIntrospectionData(final Class<?> beanClass) {
    final DefaultIntrospectionContext ictx = new DefaultIntrospectionContext(beanClass);
    for (final BeanIntrospector bi : introspectors) {
        try {
            bi.introspect(ictx);
        } catch (final IntrospectionException iex) {
            log.error("Exception during introspection", iex);
        }
    }
    return new BeanIntrospectionData(ictx.getPropertyDescriptors());
}

fetchIntrospectionData()方法中,通过内置的内省器DefaultBeanIntrospector使用java的内省API将获取的信息传递给DefaultIntrospectionContext, 在通过DefaultIntrospectionContext构造BeanIntrospectionDataDefaultBeanIntrospector具体的代码:

public void introspect(final IntrospectionContext icontext) {
    BeanInfo beanInfo = null;
    try {
        // JAVA 的 Instrospector
        beanInfo = Introspector.getBeanInfo(icontext.getTargetClass());
    } catch (final IntrospectionException e) {
        return;
    }
    PropertyDescriptor[] descriptors = beanInfo.getPropertyDescriptors();
    if (descriptors == null) {
    descriptors = new PropertyDescriptor[0];
    }
    // 解决IndexedPropertyDescriptor在不同版本的JDK下的差异
    handleIndexedPropertyDescriptors(icontext.getTargetClass(), descriptors);
    icontext.addPropertyDescriptors(descriptors);
}
3.3 判断属性是否可读/可写

要进行属性copy,那么首先得确保原始对象的属性可读、目标对象属性可写。在PropertyUtilsBean中通过isWriteable(); isReadable()方法,这两个方法看上去比较长,我们把关于exception的处理省略掉拿出来看下:

public boolean isReadable(Object bean, String name) {
    // Omit Validate method parameters
    // Resolve nested references, 解析内嵌的属性,形如 student.name
    while (resolver.hasNested(name)) {
        final String next = resolver.next(name);
        Object nestedBean = nestedBean = getProperty(bean, next);
        if (nestedBean == null) {
            throw new NestedNullException("Null property value for);
        }
        bean = nestedBean;
        name = resolver.remove(name);
    }
    // Remove any subscript from the final name value, 在最终的方法名中移除所有的下标
    name = resolver.getProperty(name);
    if (bean instanceof WrapDynaBean) {
        bean = ((WrapDynaBean)bean).getInstance();
    }
    if (bean instanceof DynaBean) {
        // All DynaBean properties are readable,所有DynaBean的属性均是可读的
        return (((DynaBean) bean).getDynaClass().getDynaProperty(name) != null);
    } else {
        final PropertyDescriptor desc = getPropertyDescriptor(bean, name);
        if (desc != null) {
            Method readMethod = getReadMethod(bean.getClass(), desc);
            if (readMethod == null) {
                if (desc instanceof IndexedPropertyDescriptor) {
                    readMethod = ((IndexedPropertyDescriptor) desc).getIndexedReadMethod();
                } else if (desc instanceof MappedPropertyDescriptor) {
                    readMethod = ((MappedPropertyDescriptor) desc).getMappedReadMethod();
                }
                readMethod = MethodUtils.getAccessibleMethod(bean.getClass(), readMethod);
            }
            return (readMethod != null);
        } else {
            return (false);
        }
    }
}

从以上代码我们可以得知,每个属性的可读、可写在每次使用时都需要获取Method,然后进行判断,并且还需要处理DynaBean、Nested的逻辑;当我们进行批量的属性copy时,依然需要执行以上步骤,并未将method的判断结果进行缓存,这也是其相比于其他的jar低效的原因.

3.4 读取原始Bean的属性值、设置目标Bean的属性值

我们还是省略掉其中的有效性判断和异常的代码:

public Object getSimpleProperty(final Object bean, final String name)
            throws IllegalAccessException, InvocationTargetException,
            NoSuchMethodException {
    // omit check null code ...
    // 校验属性
    if (resolver.hasNested(name)) {
        throw new IllegalArgumentException
                ("Nested property names are not allowed: Property '" +
                name + "' on bean class '" + bean.getClass() + "'");
    } else if (resolver.isIndexed(name)) {
        throw new IllegalArgumentException
                ("Indexed property names are not allowed: Property '" +
                name + "' on bean class '" + bean.getClass() + "'");
    } else if (resolver.isMapped(name)) {
        throw new IllegalArgumentException
                ("Mapped property names are not allowed: Property '" +
                name + "' on bean class '" + bean.getClass() + "'");
    }

    // DynaBean的特殊逻辑
    if (bean instanceof DynaBean) {
        final DynaProperty descriptor =
                ((DynaBean) bean).getDynaClass().getDynaProperty(name);
        if (descriptor == null) {
            throw new NoSuchMethodException("Unknown property '" +
                    name + "' on dynaclass '" +
                    ((DynaBean) bean).getDynaClass() + "'" );
        }
        return (((DynaBean) bean).get(name));
    }

    final PropertyDescriptor descriptor = getPropertyDescriptor(bean, name);
    if (descriptor == null) {
        throw new NoSuchMethodException("Unknown property '" +
                name + "' on class '" + bean.getClass() + "'" );
    }
    // 获取getter方法
    final Method readMethod = getReadMethod(bean.getClass(), descriptor);
    if (readMethod == null) {
        throw new NoSuchMethodException("Property '" + name +
                "' has no getter method in class '" + bean.getClass() + "'");
    }
    // 调用getter方法读取值
    final Object value = invokeMethod(readMethod, bean, EMPTY_OBJECT_ARRAY);
    return (value);
}

以上是读取属性值的方法。 读取到属性值之后,就是设置值到目标bean上了。 在BeanUtilsBean的实现中,又重复的处理了属性的内嵌逻辑与DynaBean逻辑,最终获取到其setter方法将值赋予目标Bean.

4. 源码: spring.BeanUtils

BeanUtils 位于spring-beans模块中,暴露出静态方法copyProperties用以进行属性copy,每个copyProperties最终均调用一个私有静态方法实现属性copy:

private static void copyProperties(Object source, Object target, Class<?> editable, String... ignoreProperties){
        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;
    }
    // 第一步 调用Java 内省API 获取PropertyDescriptor
    PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);
    List<String> ignoreList = (ignoreProperties != null ? Arrays.asList(ignoreProperties) : null);
    // 第二步 轮询目标bean的PropertyDescriptor
    for (PropertyDescriptor targetPd : targetPds) {
        Method writeMethod = targetPd.getWriteMethod();
        // 判断是否存在setter方法以及属性是否在需要忽略的属性列表中
        if (writeMethod != null && (ignoreList == null || !ignoreList.contains(targetPd.getName()))) {
            // 获取源bean的PropertyDescriptor
            PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName());
            if (sourcePd != null) {
                // 获取getter方法
                Method readMethod = sourcePd.getReadMethod();
                if (readMethod != null &&
                        ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())) {
                    try {
                        // 如果getter方法不是public,则需要设置其accessible
                        if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {
                            readMethod.setAccessible(true);
                        }
                        // 反射获取属性值
                        Object value = readMethod.invoke(source);
                        // 如果setter方法不是public则需要设置其accessible
                        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);
                    }
                }
            }
        }
    }
}
4.1 获取Bean的PropertyDescriptor

spring.BeanUtils中对于bean的PropertyDescriptor处理以及缓存均是由CachedIntrospectionResults类来进行处理。 CacheIntrospectionResults将数据缓存在静态集合中,使用了工厂方法的设计模式,通过forClass(Class)方法暴露缓存:

static CachedIntrospectionResults forClass(Class<?> beanClass) throws BeansException {
    // 从缓存中获取CachedIntrospectionResults
    CachedIntrospectionResults results = strongClassCache.get(beanClass);
    if (results != null) {
        return results;
    }
    // 从缓存中获取CachedIntrospectionResults
    results = softClassCache.get(beanClass);
    if (results != null) {
        return results;
    }
    // 构造CachedIntrospectionResults
    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);
}

我们可以看到此处具有两个缓存:strongClassCachesoftClassCache,那他俩什么区别呢?

首先我们看他们的定义:

static final ConcurrentMap<Class<?>, CachedIntrospectionResults> strongClassCache =
            new ConcurrentHashMap<Class<?>, CachedIntrospectionResults>(64);
static final ConcurrentMap<Class<?>, CachedIntrospectionResults> softClassCache =
            new ConcurrentReferenceHashMap<Class<?>, CachedIntrospectionResults>(64);

ConcurrentReferenceHashMap可以指定对应的引用级别,其内部采用分段锁实现,与jdk1.7的ConcurrentMap的实现原理类似。

strongClassCache中持有的缓存是强引用,而softClassCache持有的缓存是软引用 (JDK有4中引用级别,分别是强引用,软引用,弱引用以及虚引用,引用级别体现在决定GC的时候持有的实例被回收的时机)。

strongClassCache用于缓存cache-safe的bean class数据,而softClassCache用于缓存none-cache-safe bean class数据;strongClassCache中的数据与spring application的生命周期一致,而softClassCache的生命周期则不由spring进行管理,因此为了防止因classloader提前关闭导致内存泄漏,此处采用软引用进行缓存.

那什么样的数据会被cache在strongClassCache中呢?beanClass的ClassLoader与当前相同时或者与程序指定的ClassLoader相同时会被存储于strongClassCache,其余均为存储于softClassCache中。

如果从以上cache中没有拿到数据,那么会new CachedIntrospectionResults(Class),相应的调用Java Introspector的相关API均在此构造函数中:

// omit some logger code ...
private CachedIntrospectionResults(Class<?> beanClass) throws BeansException {
    try {
        BeanInfo beanInfo = null;
        // 对一些特殊的set方法(setA(int index, Object a))或者list的set方法进行处理
        for (BeanInfoFactory beanInfoFactory : beanInfoFactories) {
            beanInfo = beanInfoFactory.getBeanInfo(beanClass);
            if (beanInfo != null) {
                break;
            }
        }
        if (beanInfo == null) {
            // fall back到默认获取BeanInfo的方式
            beanInfo = (shouldIntrospectorIgnoreBeaninfoClasses ?
                    Introspector.getBeanInfo(beanClass, Introspector.IGNORE_ALL_BEANINFO) : Introspector.getBeanInfo(beanClass));
        }
        this.beanInfo = beanInfo;
        // propertyDescriptor缓存
        this.propertyDescriptorCache = new LinkedHashMap<String, PropertyDescriptor>();

        // 考虑到性能原因,对于每个PropertyDescriptor只处理一次
        PropertyDescriptor[] pds = this.beanInfo.getPropertyDescriptors();
        for (PropertyDescriptor pd : pds) {
            if (Class.class == beanClass &&
                    ("classLoader".equals(pd.getName()) ||  "protectionDomain".equals(pd.getName()))) {
                continue;
            }
            // 重新包装为GenericTypeAwarePropertyDescriptor
            pd = buildGenericTypeAwarePropertyDescriptor(beanClass, pd);
            this.propertyDescriptorCache.put(pd.getName(), pd);
        }

        // 检查Java 8在接口中的默认实现方法
        Class<?> clazz = beanClass;
        while (clazz != null) {
            Class<?>[] ifcs = clazz.getInterfaces();
            for (Class<?> ifc : ifcs) {
                BeanInfo ifcInfo = Introspector.getBeanInfo(ifc, Introspector.IGNORE_ALL_BEANINFO);
                PropertyDescriptor[] ifcPds = ifcInfo.getPropertyDescriptors();
                for (PropertyDescriptor pd : ifcPds) {
                    if (!this.propertyDescriptorCache.containsKey(pd.getName())) {
                        pd = buildGenericTypeAwarePropertyDescriptor(beanClass, pd);
                        this.propertyDescriptorCache.put(pd.getName(), pd);
                    }
                }
            }
            clazz = clazz.getSuperclass();
        }
        this.typeDescriptorCache = new ConcurrentReferenceHashMap<PropertyDescriptor, TypeDescriptor>();
    }
    catch (IntrospectionException ex) {
        throw new FatalBeanException("Failed to obtain BeanInfo for class [" + beanClass.getName() + "]", ex);
    }
}

这段代码主要的作用就是通过内省接口得到BeanInfo,然后将PropertyDescriptor缓存起来。具体流程:

  1. 首先通过BeanInfoFactory获取BeanInfo; 这里默认注册时BeanInfoFactoryExtendedBeanInfoFactory, 此类主要处理包含一些特殊set方法的bean:
public static boolean isCandidateWriteMethod(Method method) {
       String methodName = method.getName();
       Class<?>[] parameterTypes = method.getParameterTypes();
       int nParams = parameterTypes.length;
       return (methodName.length() > 3 && methodName.startsWith("set") && Modifier.isPublic(method.getModifiers()) &&
                (!void.class.isAssignableFrom(method.getReturnType()) || Modifier.isStatic(method.getModifiers())) &&
                (nParams == 1 || (nParams == 2 && int.class == parameterTypes[0])));
   }

如果一个bean中包含这么一个方法:以set开头 &&(返回值不为void || 是静态方法) && (具有一个参数 || 有两个参数其中第一个参数是int), 形如:

   // void.class.isAssignableFrom(method.getReturnType()) 方法返回值不为void
   public Bean setFoo(Foo foo) {
       this.foo = foo;
       return this;
   }
   public static void setFoos(Foo foo) {
       Bean.foo = foo;
   }
   public Bean setFoos(int index, Foo foo) {
       this.foos.set(index, foo);
       return this;
   }
  1. 如果该bean不包含以上的方法,则直接采用Java的内省API获取BeanInfo
  2. 当获取到BeanInfo之后就可以对PropertyDescriptor进行缓存了;这里会将PropertyDescriptor重新包装为GenericTypeAwarePropertyDescriptor, 进行这样封装的原因是为了重新处理BridgeMethod, 通俗点讲,就是处理当前类继承了泛型类或者实现泛型接口,那怎么识别这些方法呢?

    Bridge Method: 桥接方法是jdk引入泛型后为了与之前的jdk版本兼容,在编译时自动生成的方法。桥接方法的字节码Flag会被标记为ACC_BRIDGE (桥接方法)和ACC_SYNTHETIC (由编译器生成)。通过Method.isBridge()来判断一个方法是否为BridgeMethod。如果一个方法覆写了泛型父类或者实现了泛型接口则会生成bridge method.

public static Method findBridgedMethod(Method bridgeMethod) {
    if (bridgeMethod == null || !bridgeMethod.isBridge()) {
        return bridgeMethod;
    }
    // 获取所有与bridgeMethod名称、参数数量相匹配的方法(包括父类)
    List<Method> candidateMethods = new ArrayList<Method>();
    Method[] methods = ReflectionUtils.getAllDeclaredMethods(bridgeMethod.getDeclaringClass());
    for (Method candidateMethod : methods) {
           // candidateMethod是`Bridge Method`时将其加入候选方法列表
        if (isBridgedCandidateFor(candidateMethod, bridgeMethod)) {
            candidateMethods.add(candidateMethod);
        }
    }
    if (candidateMethods.size() == 1) {
        return candidateMethods.get(0);
    }
    // 在众候选方法中找到其BridgeMethod,如果找不到返回原方法
    Method bridgedMethod = searchCandidates(candidateMethods, bridgeMethod);
    if (bridgedMethod != null) {
        return bridgedMethod;
    }else {
        return bridgeMethod;
    }
   }
  1. 处理完类中的方法,就要处理接口中实现的方法了。 在Java8中,接口是可以有默认的方法的,举个例子:
   public interface MethodAvailable {
       default String getHello(){
           return "hello";
       }
       String setHello(String hello);
   }

对于接口中实现的方法的处理逻辑与类中实现方法的处理逻辑一致。

当进行完以上步骤后,我们就拿到了缓存有内省结果的CachedIntrospectionResults实例,然后选取对应的cahche,将结果缓存起来。(选取cahce的过程与前文读取cache的过程一致);

4.2 属性值copy

从缓存中获取到了目标类的PropertyDescriptor后,就要轮询其每一个PropertyDescriptor赋值了。

赋值的过程相对比较简单一点:

  1. 获取目标类的写方法(setter)
  2. 如果目标类的写方法不为空且此方法对应的属性并不在配置的igonreList(忽略属性列表)中,则获取源类对应属性的读方法(getter)
  3. 获取到读方法之后,需要判断读方法的返回值是否与写方法的参数是同一个类型,不同类型当然无法copy了
  4. 判断读方法是否public,如果不是,则需要设置访问权限method.setAccessible(true);(非public方法在反射访问时需要设置setAccessible(true)获取访问权限),然后调用反射执行此方法,invoke(source);
  5. 判断写方法是否public,如果不是则设置访问权限,然后将读到的值,通过放射赋给目标类invoke(taget, value);

至此,类的属性copy完成。

5. 总结

5.1 spring.BeanUtils与apache.BeanUtils的性能差异原因

在大数量copy时,apache.BeanUtils相比于spring.BeanUtils慢了近14倍,究其原因,其实在于以下几点:

  • apache.BeanUtils在实现了对每个类加载器缓存了一份BeanUtilsBean的实例,在获取此实例时会加锁(synchronized)
  • apache.BeanUtils支持了DynaBeanMap映射到Object的能力,但其在后期对于PropertyDescriptor处理时,即使我采用的是简单的Object,也会去判断DynaBeanMap,此处如果采用策略模式将其分离应该会减少很多判断的时间
  • apache.BeanUtils在每次执行属性copy时,会重新从PropertyDescriptor获取读写方法,虽然对PropertyDescriptor进行了缓存,但每次获取readMethod/writeMethod也是非常耗时的尤其是在对象实例数量较多时,此处如果对于readMethod/writeMethod进行缓存,性能应该会提升很多
  • 反观spring.BeanUtils之所以比apache.BeanUtils快,就是其对PropertyDescriptor只处理一次后缓存。 相比之下可见对于PropertyDescriptor的处理是非常耗时的。
5.2 收获

通过此次探究,了解到了以下的知识点:

  1. Java Introspectpr, 在之前用到反射的时候,都是采用比较原始的方法去获取信息然后缓存再Map中;这样的弊端就是在不同的模块都需要反射的时候,如果因沟通不畅导致另一个人也通过原始的反射接口获取类信息时,是无法利用的缓存的;采用内省的话,jdk默认会进行缓存。
  2. Bridge Method, 之前对泛型擦除的理解只停留在编译期会进行泛型擦除,了解了bridge method后,对于泛型的机制也有了更多的理解
  3. 属性copy时各方式的使用场景:
    1. 对性能要求较高的时候,推荐采用手工方法调用
    2. 一般场景推荐使用net.sf.cglib.beans.BeanCopier#copy
    3. 如果考虑到引入新jar包的风险时,推荐使用org.springframework.beans.BeanUtils.copyProperties

原文地址:https://www.cnblogs.com/kancy/p/12089126.html

时间: 2024-10-18 18:31:09

BeanUtils对象属性copy的性能对比以及源码分析的相关文章

Spring IOC 容器源码分析 - 填充属性到 bean 原始对象

1. 简介 本篇文章,我们来一起了解一下 Spring 是如何将配置文件中的属性值填充到 bean 对象中的.我在前面几篇文章中介绍过 Spring 创建 bean 的流程,即 Spring 先通过反射创建一个原始的 bean 对象,然后再向这个原始的 bean 对象中填充属性.对于填充属性这个过程,简单点来说,JavaBean 的每个属性通常都有 getter/setter 方法,我们可以直接调用 setter 方法将属性值设置进去.当然,这样做还是太简单了,填充属性的过程中还有许多事情要做.

zg手册 之 python2.7.7源码分析(2)-- python 的整数对象和字符串对象

python 中的内置对象 python 中常用的内置对象有:整数对象,字符串对象,列表对象,字典对象.这些对象在python中使用最多,所以在实现上提供缓存机制,以提高运行效率. 整数对象 (PyIntObject) python 中的整数对象是不可变对象(immutable),即创建了一个 python 整数对象之后,不能再改变该对象的值. python 为创建整数对象提供了下面三种方法,其中 PyInt_FromString 和 PyInt_FromUnicode 内部也是调用 PyInt

zg手册 之 python2.7.7源码分析(1)-- python中的对象

源代码主要目录结构 Demo: python 的示例程序 Doc: 文档 Grammar: 用BNF的语法定义了Python的全部语法,提供给解析器使用 Include: 头文件,在用c/c++编写扩展模块时使用 Lib: Python自带的标准库,用python编写的 Modules: 用c编写的内建模块的实现,zlib,md5 等 Objects: 内建对象类型的实现 list,dict 等 PC:      windows 平台相关文件 PCbuild: Microsoft Visual

【转】HashMap,ArrayMap,SparseArray源码分析及性能对比

HashMap,ArrayMap,SparseArray源码分析及性能对比 jjlanbupt 关注 2016.06.03 20:19* 字数 2165 阅读 7967评论 13喜欢 43 ArrayMap及SparseArray是android的系统API,是专门为移动设备而定制的.用于在一定情况下取代HashMap而达到节省内存的目的. 一.源码分析(由于篇幅限制,源码分析部分会放在单独的文章中)二.实现原理及数据结构对比 三.性能测试对比四.总结 一.源码分析稍后会在下一篇文章中补充(都写

jQuery.buildFragment源码分析以及在构造jQuery对象的作用

这个方法在jQuery源码中比较靠后的位置出现,主要用于两处.1是构造jQuery对象的时候使用 2.是为DOM操作提供底层支持,这也就是为什么先学习它的原因.之前的随笔已经分析过jQuery的构造函数了,也提到了有12个分支,其中有一个分支就是通过jQuery.buildFragment方法来处理的,什么情况呢?就是在处理复杂html标签的时候,例如$('<div>123</div>')这样的形式,在构造函数内部通过ret变量判断是不是简单标签,如果是就调用js的createEl

Python之美[从菜鸟到高手]--浅拷贝、深拷贝完全解读(copy源码分析)

可悲的我一直以为copy模块是用C写的,有时候需要深入了解deepcopy,文档描述的实在太简单,还是不知所云. 比如说最近看sqlmap源码中AttribDict的_deepcopy__有些疑惑, def __deepcopy__(self, memo): retVal = self.__class__() memo[id(self)] = retVal for attr in dir(self): if not attr.startswith('_'): value = getattr(se

Spring IOC 容器源码分析 - 创建原始 bean 对象

1. 简介 本篇文章是上一篇文章(创建单例 bean 的过程)的延续.在上一篇文章中,我们从战略层面上领略了doCreateBean方法的全过程.本篇文章,我们就从战术的层面上,详细分析doCreateBean方法中的一个重要的调用,即createBeanInstance方法.在本篇文章中,你将看到三种不同的构造 bean 对象的方式.你也会了解到构造 bean 对象的两种策略.如果你对这些内容感兴趣,那么不妨继续往下读.我会在代码进行大量的注解,相信能帮助你理解代码逻辑.好了,其他的就不多说了

Java中arraylist和linkedlist源码分析与性能比较

Java中arraylist和linkedlist源码分析与性能比较 1,简介 在java开发中比较常用的数据结构是arraylist和linkedlist,本文主要从源码角度分析arraylist和linkedlist的性能. 2,arraylist源码分析 Arraylist底层的数据结构是一个对象数组,有一个size的成员变量标记数组中元素的个数,如下图: * The array buffer into which the elements of the ArrayList are sto

STL源码分析--仿函数 &amp; 模板的模板参数 &amp; 临时对象

STL源码分析-使用的一些特殊语法 关于泛型编程中用到的一些特殊语法,这些语法也适用于平常的模板编程 1.  类模板中使用静态成员变量 Static成员变量在类模板中并不是很特殊,同时这个变量不属于对象,属于实例化以后的这个类类型.每一个实例化对应一个static变量 2.  类模板中可以再有模板成员 3.  模板参数可以根据前一个模板参数而设定默认值 4.  类模板可以拥有非类型的模板参数 所谓非类型的模板参数就是内建型的模板参数 Template <class T,class Alloc =