【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 來賦值,這樣程式就會很明確,重點也就聚焦在了不同的地方。