對象拷貝的應用現狀簡介:
業務系統中經常需要兩個對象進行屬性的拷貝,不能否認逐個的對象拷貝是最快速最安全的做法,但是當數據對象的屬性字段數量超過程序員的容忍的程度,代碼因此變得臃腫不堪,使用一些方便的對象拷貝工具類將是很好的選擇。
目前流行的較為公用認可的工具類:
Apache的兩個版本:(反射機制)
org.apache.commons.beanutils.PropertyUtils.copyProperties(Object dest, Object orig)
org.apache.commons.beanutils.BeanUtils.copyProperties(Object dest, Object orig)
Spring版本:(反射機制) 對應的包為:spring-beans-4.0.6.RELEASE.jar
org.springframework.beans.BeanUtils.copyProperties(Object source, Object target, Class editable, String[] ignoreProperties)
cglib版本:(使用動態代理,效率高)
net.sf.cglib.beans.BeanCopier.copy(Object paramObject1, Object paramObject2, Converter paramConverter)
原理簡介
反射類型:(apache)
都使用靜態類調用,最終轉化虛擬機中兩個單例的工具對象。
public BeanUtilsBean()
{
this(new ConvertUtilsBean(), new PropertyUtilsBean());
}
ConvertUtilsBean可以通過ConvertUtils全局自定義注冊。
ConvertUtils.register(new DateConvert(), java.util.Date.class);
PropertyUtilsBean的copyProperties方法實現了拷貝的算法。
1、 動態bean:orig instanceof DynaBean:Object value = ((DynaBean)orig).get(name);然后把value復制到動態bean類
2、 Map類型:orig instanceof Map:key值逐個拷貝
3、 其他普通類::從beanInfo【每一個對象都有一個緩存的bean信息,包含屬性字段等】取出name,然后把sourceClass和targetClass逐個拷貝
Cglib類型:BeanCopier
copier = BeanCopier.create(source.getClass(), target.getClass(), false);
copier.copy(source, target, null);
Create對象過程:產生sourceClass-》TargetClass的拷貝代理類,放入jvm中,所以創建的代理類的時候比較耗時。最好保證這個對象的單例模式,可以參照最后一部分的優化方案。
創建過程:源代碼見jdk:net.sf.cglib.beans.BeanCopier.Generator.generateClass(ClassVisitor)
1、 獲取sourceClass的所有public get 方法-》PropertyDescriptor[] getters
2、 獲取TargetClass 的所有 public set 方法-》PropertyDescriptor[] setters
3、 遍歷setters的每一個屬性,執行4和5
4、 按setters的name生成sourceClass的所有setter方法-》PropertyDescriptor getter【不符合javabean規范的類將會可能出現空指針異常】
5、 PropertyDescriptor[] setters-》PropertyDescriptor setter
6、 將setter和getter名字和類型 配對,生成代理類的拷貝方法。
Copy屬性過程:調用生成的代理類,代理類的代碼和手工操作的代碼很類似,效率非常高。
你不知道這些陷阱吧?
陷阱條件 |
Apache- PropertyUtils |
Apache- BeanUtils |
Spring- BeanUtils |
Cglib- BeanCopier |
是否可以擴展 useConvete功能 |
NO |
Yes |
Yes |
Yes,但比較難用 |
(sourceObject,targetObject)的順序 |
逆序 |
逆序 |
OK
|
OK |
對sourceObject特殊屬性的限制:(Date,BigDecimal等)【見備注1】 |
OK |
NO,異常出錯 |
OK |
OK |
相同屬性名,且類型不匹配時候的處理 【見備注2】 |
異常,拷貝部分屬性,非常危險 |
OK,並能進行初級轉換,Long和Integer互轉 |
異常,拷貝部分屬性 |
OK,但是該屬性不拷貝 |
Get和set方法不匹配的處理 【見備注3】 |
OK |
OK |
OK |
創建拷貝的時候報錯,無法拷貝任何屬性(當且僅當sourceClass的get方法超過set方法) |
備注1
對targetObject特殊屬性的限制:(Date,BigDecimal等) 原因:dateTimeConveter的conveter沒有對null值的處理 |
public class ErrorBeanUtilObject { //此處省略getter,setter方法 private String name; private java.util.Date date; } public class ErrorBeanUtilsTest { public static void main(String args[]) throws Throwable { ErrorBeanUtilObject from = new ErrorBeanUtilObject(); ErrorBeanUtilObject to = new ErrorBeanUtilObject(); //from.setDate(new java.util.Date()); from.setName("TTTT"); org.apache.commons.beanutils.BeanUtils.copyProperties(to, from);//如果from.setDate去掉,此處出現conveter異常 System.out.println(ToStringBuilder.reflectionToString(from)); System.out.println(ToStringBuilder.reflectionToString(to)); } } |
備注2
相同屬性名,且類型不匹配時候的處理 原因:這兩個工具類不支持同名異類型的匹配 !!!【包裝類Long和原始數據類型long是可以的】 |
public class TargetClass { //此處省略getter,setter方法 private Long num; private String name; } public class TargetClass { //此處省略getter,setter方法 private Long num; private String name; } public class ErrorPropertyUtilsTest { public static void main(String args[]) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { SourceClass from = new SourceClass(); from.setNum(1); from.setName("name"); TargetClass to = new TargetClass(); org.apache.commons.beanutils.PropertyUtils.copyProperties(to, from); //拋出參數不匹配異常 org.springframework.beans.BeanUtils.copyProperties(from, to); //拋出參數不匹配異常 System.out.println(ToStringBuilder.reflectionToString(from)); System.out.println(ToStringBuilder.reflectionToString(to)); } } |
備注3
Get和set方法不匹配的處理 |
public class ErrorBeanCopierTest { /** * 從該用例看出BeanCopier.create的target.class 的每一個get方法必須有隊形的set方法 * @param args */ public static void main(String args[]) { BeanCopier copier = BeanCopier.create(UnSatifisedBeanCopierObject.class, SourceClass.class,false); copier = BeanCopier.create(SourceClass.class, UnSatifisedBeanCopierObject.class, false); //此處拋出異常創建 } } class UnSatifisedBeanCopierObject { private String name; private Long num; public String getName() { return name; } public void setName(String name) { this.name = name; } public Long getNum() { return num; } // public void setNum(Long num) { // this.num = num; // } } |
一些優化和改進
增強apache的beanUtils的拷貝屬性,注冊一些新的類型轉換 |
public class BeanUtilsEx extends BeanUtils { public static void copyProperties(Object dest, Object orig) { try { BeanUtils.copyProperties(dest, orig); } catch (IllegalAccessException ex) { ex.printStackTrace(); } catch (InvocationTargetException ex) { ex.printStackTrace(); } } static { ConvertUtils.register(new DateConvert(), java.util.Date.class); ConvertUtils.register(new DateConvert(), java.sql.Date.class); ConvertUtils.register(new BigDecimalConvert(), BigDecimal.class); } } |
將beancopier做成靜態類,方便拷貝 |
public class BeanCopierUtils { public static Map<String,BeanCopier> beanCopierMap = new HashMap<String,BeanCopier>();
public static void copyProperties(Object source, Object target){ String beanKey = generateKey(source.getClass(), target.getClass()); BeanCopier copier = null; if(!beanCopierMap.containsKey(beanKey)){ copier = BeanCopier.create(source.getClass(), target.getClass(), false); beanCopierMap.put(beanKey, copier); }else{ copier = beanCopierMap.get(beanKey); } copier.copy(source, target, null); } private static String generateKey(Class<?> class1,Class<?>class2){ return class1.toString() + class2.toString(); } } |
修復beanCopier對set方法強限制的約束 |
改寫net.sf.cglib.beans.BeanCopier.Generator.generateClass(ClassVisitor)方法 將133行的 MethodInfo write = ReflectUtils.getMethodInfo(setter.getWriteMethod()); 預先存一個names2放入 /* 109 */ Map names2 = new HashMap(); /* 110 */ for (int i = 0; i < getters.length; ++i) { /* 111 */ names2.put(setters[i].getName(), getters[i]); /* */ } 調用這行代碼前判斷查詢下,如果沒有改writeMethod則忽略掉該字段的操作,這樣就可以避免異常的發生。 |