源碼解讀之(七)BeanUtils.copyProperties

一、前言

關於對象的複製我們有好幾種方式,最近在做推薦系統,有這樣的一個功能點,根據手機的唯一標識imeicopy 目標手機的 imei,置換imei 畫像。這種複製、克隆另一個對象的屬性在我們後臺開發經常遇得到。我這裏提供兩種方式:

1、實現Cloneable接口重寫clone()方法

public class Image implements Cloneable {
    private String newsId;
    private String title;
    private boolean isVideo;
    private Double showTime;
    private String tag;
    private String topicVex;
    private String classification;
    private String subClassification;

    public Image() {
    }

    // Get、Set方法省略

    @Override
    public Object clone() {
        Image image = null;
        try{
            image = (Image) super.clone();
        }catch(CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return image;
    }

}
public class CopyTest {
    public static void main(String[] args) {
        Image image = new Image();
        image.setNewsId("1001");
        image.setTitle("全明星正賽 勒布朗隊-字母哥隊");
        image.setVideo(true);
        image.setShowTime(109.48);
        image.setTag("NBA");
        image.setTopicVex("東西部對決");
        image.setClassification("NBA");
        image.setSubClassification("全明星賽");

        Image imageClone = (Image) image.clone();
        System.out.println(imageClone.toString());
    }
}

輸出結果:

Image{newsId='1001', title='全明星正賽 勒布朗隊-字母哥隊', isVideo=true, showTime=109.48, tag='NBA', topicVex='東西部對決', classification='NBA', subClassification='全明星賽'}

2、org.springframework.beans.BeanUtils 包裏的 BeanUtils.copyProperties

public class Image2 {
    private String newsId;
    private String title;
    private boolean isVideo;
    private Double showTime;
    private String tag;

    public Image2() {
    }
	
	// Get、Set方法省略
}
public class CopyTest {
    public static void main(String[] args) {
        Image image = new Image();
        image.setNewsId("1001");
        image.setTitle("全明星正賽 勒布朗隊-字母哥隊");
        image.setVideo(true);
        image.setShowTime(109.48);
        image.setTag("NBA");
        image.setTopicVex("東西部對決");
        image.setClassification("NBA");
        image.setSubClassification("全明星賽");

        Image2 targetImage = new Image2();
        BeanUtils.copyProperties(image, targetImage);
        System.out.println(targetImage.toString());
    }
}

輸出結果:

Image2{newsId='1001', title='全明星正賽 勒布朗隊-字母哥隊', isVideo=true, showTime=109.48, tag='NBA'}

這兩種方式的輸出結果,小夥伴們應該也發現了。第二種方式輸出的字段少了一些,這是因爲 Image2 裏的字段屬性比 Image 少幾個。

3、小結

如果你的目標對象不需要複製源對象中的所有屬性的話,你就用 BeanUtils.copyProperties,否則用clone()方法就能滿足。

二、源碼解析

本篇的重點是第二種方式,下面我們就來從源碼層面來分析,博主用的是 JDK 1.8

// 將給定源bean的屬性值賦值到目標bean中
public static void copyProperties(Object source, Object target, String... ignoreProperties) throws BeansException {
   copyProperties(source, target, (Class)null, ignoreProperties);
}

private static void copyProperties(Object source, Object target, @Nullable Class<?> editable, @Nullable String... ignoreProperties) throws BeansException {
    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;
    }
	
	// 1、調用Java內省API,獲取PropertyDescriptor。
    PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);
    List<String> ignoreList = ignoreProperties != null ? Arrays.asList(ignoreProperties) : null;
    PropertyDescriptor[] var7 = targetPds;
    int var8 = targetPds.length;
	
	// 2、輪詢目標bean的PropertyDescriptor
    for(int var9 = 0; var9 < var8; ++var9) {
        PropertyDescriptor targetPd = var7[var9];
        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) {
                // 獲取源bean的PropertyDescriptor
                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);
                        }
                        // 通過反射獲取source對象屬性的值
                        Object value = readMethod.invoke(source);
                        // 如果setter方法不是public則需要設置其accessible
                        if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
                            writeMethod.setAccessible(true);
                        }
						// 通過反射給target對象屬性賦值
                        writeMethod.invoke(target, value);
                    } catch (Throwable var15) {
                        throw new FatalBeanException("Could not copy property '" + targetPd.getName() + "' from source to target", var15);
                    }
                }
            }
        }
    }

}

1、斷言判空

Assert.notNull(source, "Source must not be null");
Assert.notNull(target, "Target must not be null");
public static void notNull(@Nullable Object object, String message) {
    if (object == null) {
        throw new IllegalArgumentException(message);
    }
}

斷言判空就沒什麼好說的,值爲null的話,直接把message作爲異常拋出去。

2、PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);

java內省優化工具類BeanUtils(優化內省並防止內存泄漏)

Spring中專門提供了用於緩存JavaBeanPropertyDescriptor描述信息的類——CachedIntrospectionResults。但它的forClass()(獲取對象)訪問權限時default,不能被應用代碼直接使用。但是可以通過org.springframework.beans.BeanUtils工具類來使用。

public static PropertyDescriptor[] getPropertyDescriptors(Class<?> clazz) throws BeansException {
    // 工廠模式,獲取到對應的CachedIntrospectionResults對象
    CachedIntrospectionResults cr = CachedIntrospectionResults.forClass(clazz);
    return cr.getPropertyDescriptors();
}

CachedIntrospectionResults這個類使用的是工廠模式,通過forClass()方法暴露緩存,獲取到不同的CachedIntrospectionResults對象。

實際上使用的是ConcurrentHashMap進行存儲,key就是Class對象,而valueCachedIntrospectionResults對象。

static CachedIntrospectionResults forClass(Class<?> beanClass) throws BeansException {
    // 從緩存中獲取CachedIntrospectionResults(強引用)
    CachedIntrospectionResults results = (CachedIntrospectionResults)strongClassCache.get(beanClass);
    if (results != null) {
        return results;
    } else {
        // 從緩存中獲取CachedIntrospectionResults(軟引用)
        results = (CachedIntrospectionResults)softClassCache.get(beanClass);
        if (results != null) {
            return results;
        } else {
        	// 構造CachedIntrospectionResults
            results = new CachedIntrospectionResults(beanClass);
            ConcurrentMap classCacheToUse;
            // 確保Spring框架的jar包和應用類的jar包使用的是同一個ClassLoader加載的。
            // 若是多類加載器的應用,判斷應用類使用的類加載器是否是安全的。
            if (!ClassUtils.isCacheSafe(beanClass, CachedIntrospectionResults.class.getClassLoader()) && !isClassLoaderAccepted(beanClass.getClassLoader())) {
                if (logger.isDebugEnabled()) {
                    logger.debug("Not strongly caching class [" + beanClass.getName() + "] because it is not cache-safe");
                }

                classCacheToUse = softClassCache;
            } else {
                classCacheToUse = strongClassCache;
            }

            CachedIntrospectionResults existing = (CachedIntrospectionResults)classCacheToUse.putIfAbsent(beanClass, results);
            return existing != null ? existing : results;
        }
    }
}

3、strongClassCache與softClassCache

我們可以看到此處具有兩個緩存:strongClassCachesoftClassCache,那它倆什麼區別呢?

首先我們看下它們的定義:

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

ConcurrentReferenceHashMap可以指定對應的引用級別,其內部採用分段鎖實現,與jdk1.7的ConcurrentMap的實現原理類似。

strongClassCache中持有的緩存是強引用,而softClassCache持有的緩存是軟引用 (JDK有4中引用級別,分別是強引用,軟引用,弱引用以及虛引用,引用級別體現在決定GC的時候持有的實例被回收的時機)。

strongClassCache用於緩存cache-safebean class數據,而softClassCache用於緩存none-cache-safebean class數據;

strongClassCache中的數據與spring application的生命週期一致,而softClassCache的生命週期則不由spring進行管理,因此爲了防止因classloader提前關閉導致內存泄漏,此處採用軟引用進行緩存。

那什麼樣的數據會被cachestrongClassCache中呢?beanClassClassLoader與當前相同時或者與程序指定的ClassLoader相同時會被存儲於strongClassCache,其餘均爲存儲於softClassCache中。

如果從以上cache中沒有拿到數據,那麼會new CachedIntrospectionResults(Class),相應的調用Java Introspector的相關API均在此構造函數中:

private CachedIntrospectionResults(Class<?> beanClass) throws BeansException {
    try {
        if (logger.isTraceEnabled()) {
            logger.trace("Getting BeanInfo for class [" + beanClass.getName() + "]");
        }
		
        this.beanInfo = getBeanInfo(beanClass);
        if (logger.isTraceEnabled()) {
            logger.trace("Caching PropertyDescriptors for class [" + beanClass.getName() + "]");
        }

        this.propertyDescriptorCache = new LinkedHashMap();
        // 考慮到性能原因,對於每個PropertyDescriptor只處理一次
        PropertyDescriptor[] pds = this.beanInfo.getPropertyDescriptors();
        PropertyDescriptor[] var3 = pds;
        int var4 = pds.length;

        for(int var5 = 0; var5 < var4; ++var5) {
            PropertyDescriptor pd = var3[var5];
            if (Class.class != beanClass || !"classLoader".equals(pd.getName()) && !"protectionDomain".equals(pd.getName())) {
                if (logger.isTraceEnabled()) {
                    logger.trace("Found bean property '" + pd.getName() + "'" + (pd.getPropertyType() != null ? " of type [" + pd.getPropertyType().getName() + "]" : "") + (pd.getPropertyEditorClass() != null ? "; editor [" + pd.getPropertyEditorClass().getName() + "]" : ""));
                }
				// 重新包裝爲GenericTypeAwarePropertyDescriptor
                pd = this.buildGenericTypeAwarePropertyDescriptor(beanClass, pd);
                this.propertyDescriptorCache.put(pd.getName(), pd);
            }
        }

        for(Class currClass = beanClass; currClass != null && currClass != Object.class; currClass = currClass.getSuperclass()) {
            this.introspectInterfaces(beanClass, currClass);
        }

        this.typeDescriptorCache = new ConcurrentReferenceHashMap();
    } catch (IntrospectionException var7) {
        throw new FatalBeanException("Failed to obtain BeanInfo for class [" + beanClass.getName() + "]", var7);
    }
}

這段代碼主要的作用就是通過內省接口得到BeanInfo,然後將PropertyDescriptor緩存起來。具體流程如下:

4、首先通過BeanInfoFactory獲取BeanInfo; 這裏默認註冊時BeanInfoFactoryExtendedBeanInfoFactory, 此類主要處理包含一些特殊set方法的bean

private static BeanInfo getBeanInfo(Class<?> beanClass) throws IntrospectionException {
    Iterator var1 = beanInfoFactories.iterator();

    BeanInfo beanInfo;
    // 對一些特殊的set方法(setA(int index, Object a))或者list的set方法進行處理
    do {
        if (!var1.hasNext()) {
            // fallback到默認獲取BeanInfo的方式
            return shouldIntrospectorIgnoreBeaninfoClasses ? Introspector.getBeanInfo(beanClass, 3) : Introspector.getBeanInfo(beanClass);
        }

        BeanInfoFactory beanInfoFactory = (BeanInfoFactory)var1.next();
        beanInfo = beanInfoFactory.getBeanInfo(beanClass);
    } while(beanInfo == null);

    return beanInfo;
}
@Nullable
public BeanInfo getBeanInfo(Class<?> beanClass) throws IntrospectionException {
    return this.supports(beanClass) ? new ExtendedBeanInfo(Introspector.getBeanInfo(beanClass)) : null;
}
private boolean supports(Class<?> beanClass) {
    Method[] var2 = beanClass.getMethods();
    int var3 = var2.length;

    for(int var4 = 0; var4 < var3; ++var4) {
        Method method = var2[var4];
        if (ExtendedBeanInfo.isCandidateWriteMethod(method)) {
            return true;
        }
    }

    return false;
}
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.TYPE.isAssignableFrom(method.getReturnType()) || Modifier.isStatic(method.getModifiers())) && (nParams == 1 || nParams == 2 && Integer.TYPE == 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;
}

5、如果該bean不包含以上的方法,則直接採用Java內省API獲取BeanInfo

當獲取到BeanInfo之後就可以對PropertyDescriptor進行緩存了;這裏會將PropertyDescriptor重新包裝爲GenericTypeAwarePropertyDescriptor, 進行這樣封裝的原因是爲了重新處理BridgeMethod, 通俗點講,就是處理當前類繼承了泛型類或者實現泛型接口,那怎麼識別這些方法呢?

private PropertyDescriptor buildGenericTypeAwarePropertyDescriptor(Class<?> beanClass, PropertyDescriptor pd) {
    try {
        return new GenericTypeAwarePropertyDescriptor(beanClass, pd.getName(), pd.getReadMethod(), pd.getWriteMethod(), pd.getPropertyEditorClass());
    } catch (IntrospectionException var4) {
        throw new FatalBeanException("Failed to re-introspect class [" + beanClass.getName() + "]", var4);
    }
}
public GenericTypeAwarePropertyDescriptor(Class<?> beanClass, String propertyName, @Nullable Method readMethod, @Nullable Method writeMethod, Class<?> propertyEditorClass) throws IntrospectionException {
    super(propertyName, (Method)null, (Method)null);
    this.beanClass = beanClass;
    Method readMethodToUse = readMethod != null ? BridgeMethodResolver.findBridgedMethod(readMethod) : null;
    Method writeMethodToUse = writeMethod != null ? BridgeMethodResolver.findBridgedMethod(writeMethod) : null;
    if (writeMethodToUse == null && readMethodToUse != null) {
        Method candidate = ClassUtils.getMethodIfAvailable(this.beanClass, "set" + StringUtils.capitalize(this.getName()), (Class[])null);
        if (candidate != null && candidate.getParameterCount() == 1) {
            writeMethodToUse = candidate;
        }
    }

    this.readMethod = readMethodToUse;
    this.writeMethod = writeMethodToUse;
    if (this.writeMethod != null) {
        if (this.readMethod == null) {
            Set<Method> ambiguousCandidates = new HashSet();
            Method[] var9 = beanClass.getMethods();
            int var10 = var9.length;

            for(int var11 = 0; var11 < var10; ++var11) {
                Method method = var9[var11];
                if (method.getName().equals(writeMethodToUse.getName()) && !method.equals(writeMethodToUse) && !method.isBridge() && method.getParameterCount() == writeMethodToUse.getParameterCount()) {
                    ambiguousCandidates.add(method);
                }
            }

            if (!ambiguousCandidates.isEmpty()) {
                this.ambiguousWriteMethods = ambiguousCandidates;
            }
        }

        this.writeMethodParameter = new MethodParameter(this.writeMethod, 0);
        GenericTypeResolver.resolveParameterType(this.writeMethodParameter, this.beanClass);
    }

    if (this.readMethod != null) {
        this.propertyType = GenericTypeResolver.resolveReturnType(this.readMethod, this.beanClass);
    } else if (this.writeMethodParameter != null) {
        this.propertyType = this.writeMethodParameter.getParameterType();
    }

    this.propertyEditorClass = propertyEditorClass;
}

Bridge Method: 橋接方法是JDK引入泛型後爲了與之前的JDK版本兼容,在編譯時自動生成的方法。橋接方法的字節碼Flag會被標記爲ACC_BRIDGE (橋接方法) 和ACC_SYNTHETIC (由編譯器生成)。通過Method.isBridge()來判斷一個方法是否爲BridgeMethod。如果一個方法覆寫了泛型父類或者實現了泛型接口則會生成bridge method

public static Method findBridgedMethod(Method bridgeMethod) {
    if (!bridgeMethod.isBridge()) {
        return bridgeMethod;
    } else {
        // 獲取所有與bridgeMethod名稱、參數數量相匹配的方法(包括父類)
        List<Method> candidateMethods = new ArrayList();
        Method[] methods = ReflectionUtils.getAllDeclaredMethods(bridgeMethod.getDeclaringClass());
        Method[] var3 = methods;
        int var4 = methods.length;

        for(int var5 = 0; var5 < var4; ++var5) {
            Method candidateMethod = var3[var5];
            // candidateMethod是`Bridge Method`時將其加入候選方法列表
            if (isBridgedCandidateFor(candidateMethod, bridgeMethod)) {
                candidateMethods.add(candidateMethod);
            }
        }

        if (candidateMethods.size() == 1) {
            return (Method)candidateMethods.get(0);
        } else {
            // 在衆候選方法中找到其BridgeMethod,如果找不到返回原方法
            Method bridgedMethod = searchCandidates(candidateMethods, bridgeMethod);
            if (bridgedMethod != null) {
                return bridgedMethod;
            } else {
                return bridgeMethod;
            }
        }
    }
}

當進行完以上步驟後,我們就拿到了緩存有內省結果的CachedIntrospectionResults實例,然後選取對應的cahche,將結果緩存起來。(選取cahce的過程與前文讀取cache的過程一致);

6、屬性值copy

從緩存中獲取到了目標類的PropertyDescriptor後,就要輪詢其每一個PropertyDescriptor賦值了。

賦值的過程相對比較簡單一點:

  • 獲取目標類的寫方法(setter)。
  • 如果目標類的寫方法不爲空且此方法對應的屬性並不在配置的igonreList(忽略屬性列表)中,則獲取源類對應屬性的讀方法(getter)。
  • 獲取到讀方法之後,需要判斷讀方法的返回值是否與寫方法的參數是同一個類型,不同類型當然無法copy了。
  • 判斷讀方法是否public,如果不是,則需要設置訪問權限method.setAccessible(true);(非public方法在反射訪問時需要設置setAccessible(true)獲取訪問權限),然後調用反射執行此方法,invoke(source)
  • 判斷寫方法是否public,如果不是則設置訪問權限,然後將讀到的值,通過放射賦給目標類invoke(taget, value)

至此,類的屬性copy完成。

三、總結

1、 CachedIntrospectionResults防止內存泄漏這一塊源碼還未詳細分析,後續有空繼續跟進。

2、Java Introspect, 在之前用到反射的時候,都是採用比較原始的方法去獲取信息然後緩存再Map中;這樣的弊端就是在不同的模塊都需要反射的時候,如果因溝通不暢導致另一個人也通過原始的反射接口獲取類信息時,是無法利用的緩存的;採用內省的話,JDK默認會進行緩存。

3、Bridge Method, 之前對泛型擦除的理解只停留在編譯期會進行泛型擦除,瞭解了bridge method後,對於泛型的機制也有了更多的理解。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章