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

博客园 · · 1979 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

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

本文来自:博客园

感谢作者:博客园

查看原文:BeanUtils对象属性copy的性能对比以及源码分析 - kancy

1979 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传