spring.BeanUtils与apache.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());
  • 8
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值