一、前言
關於對象的複製我們有好幾種方式,最近在做推薦系統,有這樣的一個功能點,根據手機的唯一標識imei
,copy
目標手機的 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
中專門提供了用於緩存JavaBean
的PropertyDescriptor
描述信息的類——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
對象,而value
是CachedIntrospectionResults
對象。
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
我們可以看到此處具有兩個緩存:strongClassCache
與softClassCache
,那它倆什麼區別呢?
首先我們看下它們的定義:
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-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
均在此構造函數中:
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
; 這裏默認註冊時BeanInfoFactory
是ExtendedBeanInfoFactory
, 此類主要處理包含一些特殊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
後,對於泛型的機制也有了更多的理解。