【Java 對象拷貝機制】使用 CGlib 實現 Bean 拷貝(BeanCopier)

語言: CN / TW / HK

【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)

工具操作

img

原理簡介

反射類型:(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)

  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 屬性過程:調用生成的代理類,代理類的代碼和手工操作的代碼很類似,效率非常高。

上述這幾種方式速度最快的是 BeanCopier,默認只複製名稱和類型相同的字段,還會對 date 為空的情況不進行復制。

我認為這樣做最好,比如對象 A 的值複製到 B 中,我們把相同的進行復制,把不同的,也就是需要我們個性化的一些字段,單獨出來用 get 來賦值,這樣程序就會很明確,重點也就聚焦在了不同的地方。