BeanUtils對象屬性copy的性能對比以及源碼分析

轉載:https://www.cnblogs.com/kancy/p/12089126.html

在日常編碼中,經常會遇到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.3org.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|02. 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|03. 源碼: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中的屬性進行匹配,然後賦值;
  • DynaBean :DynaBean顧名思義,它是一種可以形成動態java bean的對象,也就是說它內部會存儲屬性名稱、類型以及對應的值,在copy屬性時也是將其內部的屬性名稱與dest對象的屬性名稱對應後賦值;
  • 標準Java Bean :這個是我們主要進行分析的類型,它是標準的JavaBean對象;與前兩者的差異只是在於對原始bean的取值的處理上.

3|13.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|23.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|33.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|43.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|04. 源碼: 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|14.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|24.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|05. 總結

5|15.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|25.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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章