【Java 對(duì)象拷貝機(jī)制】使用 CGlib 實(shí)現(xiàn) Bean 拷貝(BeanCopier)

對(duì)象拷貝現(xiàn)狀

業(yè)務(wù)系統(tǒng)中經(jīng)常需要兩個(gè)對(duì)象進(jìn)行屬性的拷貝,不能否認(rèn)逐個(gè)的對(duì)象拷貝是最快速最安全的做法,但是當(dāng)數(shù)據(jù)對(duì)象的屬性字段數(shù)量超過程序員的容忍的程度,代碼因此變得臃腫不堪,使用一些方便的對(duì)象拷貝工具類將是很好的選擇。

模型數(shù)據(jù)轉(zhuǎn)換

項(xiàng)目中或多或少會(huì)對(duì)某些實(shí)體進(jìn)行轉(zhuǎn)換(DTO、VO、DO 或者 PO 等),往往具有相同的屬性名稱,數(shù)量少的情況下我們可以直接采取 set、get 方法進(jìn)行賦值,可是如果這樣的轉(zhuǎn)換在很多地方都會(huì)用到,還是靠 set 來進(jìn)行操作勢必會(huì)大大的影響開發(fā)效率。

  • 關(guān)于實(shí)體轉(zhuǎn)換,我們把一個(gè)實(shí)體對(duì)應(yīng)一張表(這可以當(dāng)成 DO)。
  • 業(yè)務(wù)中與第三方進(jìn)行數(shù)據(jù)交互,我們需要把實(shí)體的數(shù)據(jù)傳給他們,但不一定是一個(gè) DO 中的所有屬性可能減少或者多個(gè) DO 中的屬性組成,這里我們引入 DTO(這個(gè)實(shí)體中我們可以去除一些隱私信息,比如:銀行卡號(hào),身份證,密碼)。
  • 一個(gè)性別我們用 1、2 表示男女,頁面中不能直接顯示 1 或者 2,需要顯示男、女或者靚仔(男)、靚妹(女),這時(shí)候代表這樣的一個(gè)實(shí)體我們可以看作 VO。

目前流行的較為公用認(rèn)可的工具類:

Apache 的兩個(gè)版本:(反射機(jī)制)

  • org.apache.commons.beanutils.PropertyUtils.copyProperties(Object dest, Object orig)

原因:dateTimeConveter 的 conveter 沒有對(duì) 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去掉,此處出現(xiàn)conveter異常
     System.out.println(ToStringBuilder.reflectionToString(from));    
     System.out.println(ToStringBuilder.reflectionToString(to));
     }
}
  • org.apache.commons.beanutils.BeanUtils.copyProperties(Object dest, Object orig)
  • 相同屬性名,且類型不匹配時(shí)候的處理
  • 原因:這兩個(gè)工具類不支持同名異類型的匹配 !!!【包裝類 Long 和原始數(shù)據(jù)類型 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();
        //拋出參數(shù)不匹配異常
        org.apache.commons.beanutils.PropertyUtils.copyProperties(to, from);
        org.springframework.beans.BeanUtils.copyProperties(from, to);
        //拋出參數(shù)不匹配異常
        System.out.println(ToStringBuilder.reflectionToString(from));
        System.out.println(ToStringBuilder.reflectionToString(to));
    }
}

Spring 版本:(反射機(jī)制)

  • org.springframework.beans.BeanUtils.copyProperties(Object source, Object target, Class editable, String[] ignoreProperties)

cglib 版本:(使用動(dòng)態(tài)代理,效率高)

cglib 是一款比較底層的操作 java 字節(jié)碼的框架

  • net.sf.cglib.beans.BeanCopier.copy(Object paramObject1, Object paramObject2, Converter paramConverter)

工具操作

f6828b0ca21a63287fd6187d4e797ade.png

原理簡介

反射類型:(apache)

都使用靜態(tài)類調(diào)用,最終轉(zhuǎn)化虛擬機(jī)中兩個(gè)單例的工具對(duì)象。

public BeanUtilsBean(){
    this(new ConvertUtilsBean(), new PropertyUtilsBean());
}
  • ConvertUtilsBean 可以通過 ConvertUtils 全局自定義注冊(cè)。
  • ConvertUtils.register(new DateConvert(), java.util.Date.class);
  • PropertyUtilsBean 的 copyProperties 方法實(shí)現(xiàn)了拷貝的算法。
  1. 動(dòng)態(tài) bean:orig instanceof DynaBean:Object value = ((DynaBean)orig).get(name); 然后把 value 復(fù)制到動(dòng)態(tài) bean 類。

  2. Map 類型:orig instanceof Map:key 值逐個(gè)拷貝

  3. 其他普通類:從 beanInfo【每一個(gè)對(duì)象都有一個(gè)緩存的 bean 信息,包含屬性字段等】取出 name,然后把 sourceClass 和 targetClass 逐個(gè)拷貝。

Cglib 類型:BeanCopier

copier = BeanCopier.create(source.getClass(), target.getClass(), false);
copier.copy(source, target, null);

Get 和 set 方法不匹配的處理

public class BeanCopierTest {
    /**
    * 從該用例看出BeanCopier.create的target.class 的每一個(gè)get方法必須有隊(duì)形的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); //此處拋出異常創(chuàng)建
    }
}
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 對(duì)象過程:產(chǎn)生 sourceClass-> TargetClass 的拷貝代理類,放入 jvm 中,所以創(chuàng)建的代理類的時(shí)候比較耗時(shí)。最好保證這個(gè)對(duì)象的單例模式,可以參照最后一部分的優(yōu)化方案。

創(chuàng)建過程 -> 源代碼見 jdk:

net.sf.cglib.beans.BeanCopier.Generator.generateClass(ClassVisitor)

  1. 獲取 sourceClass 的所有 public get 方法-》PropertyDescriptor[] getters
  2. 獲取 TargetClass 的所有 public set 方法-》PropertyDescriptor[] setters
  3. 遍歷 setters 的每一個(gè)屬性,執(zhí)行 4 和 5
  4. 按 setters 的 name 生成 sourceClass 的所有 setter 方法-》PropertyDescriptor getter【不符合 javabean 規(guī)范的類將會(huì)可能出現(xiàn)空指針異?!?/li>
  5. PropertyDescriptor[] setters-》PropertyDescriptor setter
  6. 將 setter 和 getter 名字和類型 配對(duì),生成代理類的拷貝方法。

原理總結(jié)

Copy 屬性過程:調(diào)用生成的代理類,代理類的代碼和手工操作的代碼很類似,效率非常高。

上述這幾種方式速度最快的是 BeanCopier,默認(rèn)只復(fù)制名稱和類型相同的字段,還會(huì)對(duì) date 為空的情況不進(jìn)行復(fù)制。

我認(rèn)為這樣做最好,比如對(duì)象 A 的值復(fù)制到 B 中,我們把相同的進(jìn)行復(fù)制,把不同的,也就是需要我們個(gè)性化的一些字段,單獨(dú)出來用 get 來賦值,這樣程序就會(huì)很明確,重點(diǎn)也就聚焦在了不同的地方。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容