【Java 對象拷貝機制】使用 CGlib 實現 Bean 拷貝(BeanCopier)
對象拷貝現狀
業務系統中經常需要兩個對象進行屬性的拷貝,不能否認逐個的對象拷貝是最快速最安全的做法,但是當數據對象的屬性字段數量超過程序員的容忍的程度,代碼因此變得臃腫不堪,使用一些方便的對象拷貝工具類將是很好的選擇。
模型數據轉換
項目中或多或少會對某些實體進行轉換(DTO、VO、DO 或者 PO 等),往往具有相同的屬性名稱,數量少的情況下我們可以直接採取 set、get 方法進行賦值,可是如果這樣的轉換在很多地方都會用到,還是靠 set 來進行操作勢必會大大的影響開發效率。
- 關於實體轉換,我們把一個實體對應一張表(這可以當成 DO)。
- 業務中與第三方進行數據交互,我們需要把實體的數據傳給他們,但不一定是一個 DO 中的所有屬性可能減少或者多個 DO 中的屬性組成,這裏我們引入 DTO(這個實體中我們可以去除一些隱私信息,比如:銀行卡號,身份證,密碼)。
- 一個性別我們用 1、2 表示男女,頁面中不能直接顯示 1 或者 2,需要顯示男、女或者靚仔(男)、靚妹(女),這時候代表這樣的一個實體我們可以看作 VO。
目前流行的較為公用認可的工具類:
Apache 的兩個版本:(反射機制)
- org.apache.commons.beanutils.PropertyUtils.copyProperties(Object dest, Object orig)
原因:dateTimeConveter 的 conveter 沒有對 null 值的處理
// targetObject特殊屬性的限制:(Date,BigDecimal等)
public class BeanObject { //此處省略getter,setter方法
private String name;
private java.util.Date date;
}
public class BeanObjectTest {
public static void main(String args[]) throws Throwable {
BeanObject from = new BeanObject();
BeanObject to = new BeanObject();
//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));
}
}
- org.apache.commons.beanutils.BeanUtils.copyProperties(Object dest, Object orig)
- 相同屬性名,且類型不匹配時候的處理
- 原因:這兩個工具類不支持同名異類型的匹配 !!!【包裝類 Long 和原始數據類型 long 是可以的】
public class SourceClass { //此處省略getter,setter方法
private Long num;
private String name;
}
public class TargetClass { //此處省略getter,setter方法
private Long num;
private String name;
}
public class PropertyUtilsTest {
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));
}
}
Spring 版本:(反射機制)
- org.springframework.beans.BeanUtils.copyProperties(Object source, Object target, Class editable, String[] ignoreProperties)
cglib 版本:(使用動態代理,效率高)
cglib 是一款比較底層的操作 java 字節碼的框架
- 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 方法實現了拷貝的算法。
-
動態 bean:orig instanceof DynaBean:Object value = ((DynaBean)orig).get(name); 然後把 value 複製到動態 bean 類。
- Map 類型:orig instanceof Map:key 值逐個拷貝
- 其他普通類:從 beanInfo【每一個對象都有一個緩存的 bean 信息,包含屬性字段等】取出 name,然後把 sourceClass 和 targetClass 逐個拷貝。
Cglib 類型:BeanCopier
copier = BeanCopier.create(source.getClass(), target.getClass(), false);
copier.copy(source, target, null);
Get 和 set 方法不匹配的處理
public class BeanCopierTest {
/**
* 從該用例看出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() {undefined
return name;
}
public void setName(String name) {undefined
this.name = name;
}
public Long getNum() {undefined
return num;
}
// public void setNum(Long num) {undefined
// this.num = num;
// }
}
Create 對象過程:產生 sourceClass-> TargetClass 的拷貝代理類,放入 jvm 中,所以創建的代理類的時候比較耗時。最好保證這個對象的單例模式,可以參照最後一部分的優化方案。
創建過程 -> 源代碼見 jdk:
net.sf.cglib.beans.BeanCopier.Generator.generateClass(ClassVisitor)
- 獲取 sourceClass 的所有 public get 方法-》PropertyDescriptor[] getters
- 獲取 TargetClass 的所有 public set 方法-》PropertyDescriptor[] setters
- 遍歷 setters 的每一個屬性,執行 4 和 5
- 按 setters 的 name 生成 sourceClass 的所有 setter 方法-》PropertyDescriptor getter【不符合 javabean 規範的類將會可能出現空指針異常】
- PropertyDescriptor[] setters-》PropertyDescriptor setter
- 將 setter 和 getter 名字和類型 配對,生成代理類的拷貝方法。
原理總結
Copy 屬性過程:調用生成的代理類,代理類的代碼和手工操作的代碼很類似,效率非常高。
上述這幾種方式速度最快的是 BeanCopier,默認只複製名稱和類型相同的字段,還會對 date 為空的情況不進行復制。
我認為這樣做最好,比如對象 A 的值複製到 B 中,我們把相同的進行復制,把不同的,也就是需要我們個性化的一些字段,單獨出來用 get 來賦值,這樣程序就會很明確,重點也就聚焦在了不同的地方。