BeanCopier、BeanUtils對(duì)象屬性拷貝

在做業(yè)務(wù)的時(shí)候靴姿,為了隔離變化沃但,我們會(huì)將DAO查詢(xún)出來(lái)的DO和對(duì)前端提供的DTO隔離開(kāi)來(lái),它們的結(jié)構(gòu)都是類(lèi)似的佛吓。寫(xiě)很多冗長(zhǎng)的b.setFiled(a.getFiled())這樣的代碼宵晚,是繁瑣無(wú)意義的。于是需要簡(jiǎn)化對(duì)象拷貝方式维雇。大多時(shí)候使用的是Apache或Spring的BeanUtils淤刃,還有另一個(gè)更高效的屬性拷貝方式:BeanCopier。

一吱型、背景

1??對(duì)象拷貝概念
Java中逸贾,數(shù)據(jù)類(lèi)型分為值類(lèi)型(基本數(shù)據(jù)類(lèi)型)和引用類(lèi)型。對(duì)象拷貝分為淺拷貝(淺克隆)與深拷貝(深克隆)津滞。

淺拷貝與深拷貝差異

2??示例前準(zhǔn)備铝侵。源對(duì)象屬性類(lèi)UserDO.class(以下示例,源對(duì)象都用這個(gè))

@Data
public class UserDO {    
  private int id;    
  private String userName;    
  private LocalDateTime gmtBroth;    
  private BigDecimal balance;    
  public UserDO(Integer id, String userName, LocalDateTime gmtBroth, BigDecimal balance) {        
    this.id = id;        
    this.userName = userName;        
    this.gmtBroth = gmtBroth;        
    this.balance = balance;    
  }
}

造數(shù)據(jù)工具類(lèi)DataUtil

public class DataUtil {    
  /**    
     * 模擬查詢(xún)出一條數(shù)據(jù)     
      * @return     
      */    
  public static UserDO createData() {        
    return new UserDO(1, "Van", LocalDateTime.now(),new BigDecimal(100L));    
  }    
  /**       
      * 模擬查詢(xún)出多條數(shù)據(jù)     
      * @param num 數(shù)量     
      * @return     
      */    
  public static List<UserDO> createDataList(int num) {        
    List<UserDO> userDOS = new ArrayList<>();        
    for (int i = 0; i < num; i++) {            
    UserDO userDO = new UserDO(i+1, "Van", LocalDateTime.now(),new BigDecimal(100L));            
    userDOS.add(userDO);        
  }        
    return userDOS;    
 }
}

二触徐、對(duì)象拷貝之BeanUtils

Apache和Spring均有BeanUtils工具類(lèi)咪鲜, Apache的BeanUtils穩(wěn)定性與效率都不行;Spring的BeanUtils比較穩(wěn)定撞鹉,不會(huì)因?yàn)榱看罅伺北臅r(shí)明顯增加颖侄,故一般都使用Spring的BeanUtils。

1??源碼解讀
Spring中的BeanUtils享郊,其中實(shí)現(xiàn)的方式很簡(jiǎn)單览祖,就是對(duì)兩個(gè)對(duì)象中相同名字的屬性進(jìn)行簡(jiǎn)單get/set,僅檢查屬性的可訪(fǎng)問(wèn)性炊琉。成員變量賦值是基于目標(biāo)對(duì)象的成員列表展蒂,并且會(huì)跳過(guò)ignore的以及在源對(duì)象中不存在的,所以這個(gè)方法是安全的温自,不會(huì)因?yàn)閮蓚€(gè)對(duì)象之間的結(jié)構(gòu)差異導(dǎo)致錯(cuò)誤玄货,但是必須保證同名的兩個(gè)成員變量類(lèi)型相同皇钞。

2??示例

@Slf4j
public class BeanUtilsDemo {    
  public static void main(String[] args) {        
    long start = System.currentTimeMillis();        
    UserDO userDO = DataUtil.createData();        
    log.info("拷貝前悼泌,userDO:{}", userDO);        
    UserDTO userDTO = new UserDTO();
    BeanUtils.copyProperties(userDO,userDTO);        
    log.info("拷貝后,userDO:{}", userDO);
  long end = System.currentTimeMillis();  
   }
}

結(jié)果

18:12:11.734 [main] INFO cn.van.parameter.bean.copy.demo.BeanUtilsDemo 
- 拷貝前夹界,userDO:UserDO(id=1, userName=Van, gmtBroth=2019-11-02T18:12:11.730, balance=100)
18:12:11.917 [main] INFO cn.van.parameter.bean.copy.demo.BeanUtilsDemo 
- 拷貝后馆里,userDO:UserDO(id=1, userName=Van, gmtBroth=2019-11-02T18:12:11.730, balance=100)

三、對(duì)象拷貝之BeanCopier

BeanCopier是用于在兩個(gè)bean之間進(jìn)行屬性拷貝的可柿。BeanCopier支持兩種方式:
1??一種是不使用Converter的方式鸠踪,僅對(duì)兩個(gè)bean間屬性名和類(lèi)型完全相同的變量進(jìn)行拷貝;
2??另一種則引入Converter复斥,可以對(duì)某些特定屬性值進(jìn)行特殊操作营密。

基本使用

  • 依賴(lài)
<dependency>    
<groupId>cglib</groupId>    
<artifactId>cglib-nodep</artifactId>    
<version>3.3.0</version>
</dependency>

注意:該依賴(lài)非必須,因?yàn)镾pring中已經(jīng)集成了cglib目锭,本文使用的就是org.springframework.cglib.beans.BeanCopier评汰。

3.1.1 屬性名稱(chēng)、類(lèi)型都相同

  • 目標(biāo)對(duì)象屬性類(lèi)
@Data
public class UserDTO {    
  private int id;    
  private String userName;
}
  • 測(cè)試方法
/** * 屬性名稱(chēng)痢虹、類(lèi)型都相同(部分屬性不拷貝) */
private static void normalCopy() {  
// 模擬查詢(xún)出數(shù)據(jù)    
UserDO userDO = DataUtil.createData();    
log.info("拷貝前被去,userDO:{}", userDO);    
//第一個(gè)參數(shù):源對(duì)象。
//第二個(gè)參數(shù):目標(biāo)對(duì)象奖唯。
//第三個(gè)參數(shù):是否使用自定義轉(zhuǎn)換器(下面會(huì)介紹)惨缆,下同    
BeanCopier b = BeanCopier.create(UserDO.class, UserDTO.class, false);    
UserDTO userDTO = new UserDTO();    
b.copy(userDO, userDTO, null);    
log.info("拷貝后,userDTO:{}", userDTO);
}
  • 結(jié)果:拷貝成功
18:24:24.080 [main] INFO cn.van.parameter.bean.copy.demo.BeanCopierDemo 
- 拷貝前丰捷,userDO:UserDO(id=1, userName=Van, gmtBroth=2019-11-02T18:24:24.077, balance=100)
18:24:24.200 [main] INFO cn.van.parameter.bean.copy.demo.BeanCopierDemo 
- 拷貝后坯墨,userDTO:UserDTO(id=1, userName=Van)

3.1.2 屬性名稱(chēng)相同、類(lèi)型不同

  • 目標(biāo)對(duì)象屬性類(lèi)
@Data
public class UserEntity {    
  private Integer id;    
  private String userName;
}
  • 測(cè)試方法
/** * 屬性名稱(chēng)相同病往、類(lèi)型不同 */
private static void sameNameDifferentType() {    
// 模擬查詢(xún)出數(shù)據(jù)    
UserDO userDO = DataUtil.createData();    
log.info("拷貝前畅蹂,userDO:{}", userDO);    
BeanCopier b = BeanCopier.create(UserDO.class, UserEntity.class, false);
UserEntity userEntity = new UserEntity();    
b.copy(userDO, userEntity, null);    
log.info("拷貝后,userEntity:{}", userEntity);}
  • 結(jié)果
19:43:31.645 [main] INFO cn.van.parameter.bean.copy.demo.BeanCopierDemo 
- 拷貝前荣恐,userDO:UserDO(id=1, userName=Van, gmtBroth=2019-11-02T19:43:31.642, balance=100)
19:43:31.748 [main] INFO cn.van.parameter.bean.copy.demo.BeanCopierDemo 
- 拷貝后液斜,userEntity:UserEntity(id=null, userName=Van)
  • 分析
    通過(guò)日志可以發(fā)現(xiàn):UserDO的int類(lèi)型的id無(wú)法拷貝到UserEntity的Integer的id累贤。

3.1.3 小節(jié)

BeanCopier只拷貝名稱(chēng)和類(lèi)型都相同的屬性。

即使源類(lèi)型是原始類(lèi)型(int, short和char等)少漆,目標(biāo)類(lèi)型是其包裝類(lèi)型(Integer, Short和Character等)臼膏,或反之:都不會(huì)被拷貝。

3.2 自定義轉(zhuǎn)換器

通過(guò)3.1.2可知示损,當(dāng)源和目標(biāo)類(lèi)的屬性類(lèi)型不同時(shí)渗磅,不能拷貝該屬性,此時(shí)我們可以通過(guò)實(shí)現(xiàn)Converter接口來(lái)自定義轉(zhuǎn)換器

3.2.1 準(zhǔn)備

  • 目標(biāo)對(duì)象屬性類(lèi)
@Data
public class UserDomain {    
  private Integer id;    
  private String userName;        
/**     
    * 以下兩個(gè)字段用戶(hù)模擬自定義轉(zhuǎn)換    
    */    
  private String gmtBroth;    
  private String balance;
}

3.2.2 不使用Converter

  • 測(cè)試方法
/** * 類(lèi)型不同,不使用Converter */
public static void noConverterTest() {    
  // 模擬查詢(xún)出數(shù)據(jù)    
  UserDO userDO = DataUtil.createData();   
  log.info("拷貝前检访,userDO:{}", userDO);    
  BeanCopier copier = BeanCopier.create(UserDO.class, UserDomain.class, false);
  UserDomain userDomain = new UserDomain();    
  copier.copy(userDO, userDomain, null);    
  log.info("拷貝后始鱼,userDomain:{}", userDomain);}
  • 結(jié)果
19:49:19.294 [main] INFO cn.van.parameter.bean.copy.demo.BeanCopierDemo 
- 拷貝前,userDO:UserDO(id=1, userName=Van, gmtBroth=2019-11-02T19:49:19.290, balance=100)
19:49:19.394 [main] INFO cn.van.parameter.bean.copy.demo.BeanCopierDemo 
- 拷貝后脆贵,userDomain:UserDomain(id=null, userName=Van, gmtBroth=null, balance=null)
  • 分析
    通過(guò)打印日志的前后對(duì)比医清,屬性類(lèi)型不同的字段id,gmtBroth,balance未拷貝卖氨。

3.2.3 使用Converter

  • 實(shí)現(xiàn)Converter接口來(lái)自定義屬性轉(zhuǎn)換
public  class UserConverter implements Converter {    
   /**    
     * 時(shí)間轉(zhuǎn)換的格式    
     */    
  DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");    
  /**    
    * 自定義屬性轉(zhuǎn)換    
    * @param value 源對(duì)象屬性類(lèi)     
    * @param target 目標(biāo)對(duì)象里屬性對(duì)應(yīng)set方法名,eg.setId     
    * @param context 目標(biāo)對(duì)象屬性類(lèi)   
    * @return     
    */    
  @Override
  public Object convert(Object value, Class target, Object context) {     
     if (value instanceof Integer) {            
        return value;      
     } else if (value instanceof LocalDateTime) {      
        LocalDateTime date = (LocalDateTime) value;      
        return dtf.format(date);     
     } else if (value instanceof BigDecimal) {            
        BigDecimal bd = (BigDecimal) value;       
        return bd.toPlainString();      
     }       
   return value;  
  }
}
  • 測(cè)試方法
/** 
  * 類(lèi)型不同,使用Converter 
  */
public static void converterTest() {    
  // 模擬查詢(xún)出數(shù)據(jù)    
  UserDO userDO = DataUtil.createData();    
  log.info("拷貝前会烙,userDO:{}", userDO);    
  BeanCopier copier = BeanCopier.create(UserDO.class, UserDomain.class, true);
  UserConverter converter = new UserConverter();    
  UserDomain userDomain = new UserDomain();    
  copier.copy(userDO, userDomain, converter);    
  log.info("拷貝后:userDomain:{}", userDomain);
}
  • 結(jié)果:全部拷貝
19:51:11.989 [main] INFO cn.van.parameter.bean.copy.demo.BeanCopierDemo 
- 拷貝前,userDO:UserDO(id=1, userName=Van, gmtBroth=2019-11-02T19:51:11.985, balance=100)

3.2.4 小節(jié)

  1. 一旦使用Converter筒捺,BeanCopier只使用Converter定義的規(guī)則去拷貝屬性柏腻,所以在convert()方法中要考慮所有的屬性。
  2. 但使用Converter會(huì)使對(duì)象拷貝速度變慢系吭。

3.3 BeanCopier總結(jié)

  1. 當(dāng)源類(lèi)和目標(biāo)類(lèi)的屬性名稱(chēng)五嫂、類(lèi)型都相同,拷貝沒(méi)問(wèn)題肯尺。
  2. 當(dāng)源對(duì)象和目標(biāo)對(duì)象的屬性名稱(chēng)相同沃缘、類(lèi)型不同,那么名稱(chēng)相同而類(lèi)型不同的屬性不會(huì)被拷貝。注意蟆盹,原始類(lèi)型(int孩灯,short,char)和 他們的包裝類(lèi)型逾滥,在這里都被當(dāng)成了不同類(lèi)型峰档,因此不會(huì)被拷貝。
  3. 源類(lèi)或目標(biāo)類(lèi)的setter比getter少寨昙,拷貝沒(méi)問(wèn)題讥巡,此時(shí)setter多余,但是不會(huì)報(bào)錯(cuò)舔哪。
  4. 源類(lèi)和目標(biāo)類(lèi)有相同的屬性(兩者的getter都存在)欢顷,但是目標(biāo)類(lèi)的setter不存在,此時(shí)會(huì)拋出NullPointerException捉蚤。

四抬驴、BeanUtils與BeanCopier速度對(duì)比

4.1 BeanUtils

  • 測(cè)試代碼
private static void beanUtil() {    
  List<UserDO> list = DataUtil.createDataList(10000);    
  long start = System.currentTimeMillis();    
  List<UserDTO> dtoList = new ArrayList<>();    
  list.forEach(userDO -> {  UserDTO userDTO = new UserDTO();
  BeanUtils.copyProperties(userDO,userDTO);
  dtoList.add(userDTO);    
  });    
  log.info("BeanUtils cotTime: {}ms", System.currentTimeMillis() - start);
}
  • 結(jié)果(耗時(shí):232ms)
20:14:24.380 [main] INFO cn.van.parameter.bean.copy.demo.BeanCopyComparedDemo 
- BeanUtils cotTime: 232ms

4.2 BeanCopier

  • 測(cè)試代碼
private static void beanCopier() {    
  // 工具類(lèi)生成10w條數(shù)據(jù)    
  List<UserDO> doList = DataUtil.createDataList(10000);    
  long start = System.currentTimeMillis();    
  List<UserDTO> dtoList = new ArrayList<>();    
  doList.forEach(userDO -> {        
  BeanCopier b = BeanCopier.create(UserDO.class, UserDTO.class, false);
  UserDTO userDTO = new UserDTO();        
  b.copy(userDO, userDTO, null);        
  dtoList.add(userDTO);    });    
  log.info("BeanCopier costTime: {}ms", System.currentTimeMillis() - start);
}
  • 結(jié)果(耗時(shí):116ms)
20:15:24.380 [main] INFO cn.van.parameter.bean.copy.demo.BeanCopyComparedDemo 
- BeanCopier costTime: 116ms

4.3 緩存BeanCopier實(shí)例提升性能

BeanCopier拷貝速度快炼七,性能瓶頸出現(xiàn)在創(chuàng)建BeanCopier實(shí)例的過(guò)程中。 所以布持,把創(chuàng)建過(guò)的BeanCopier實(shí)例放到緩存中豌拙,下次可以直接獲取,提升性能题暖。

  • 測(cè)試代碼
private static void beanCopierWithCache() {    
    List<UserDO> userDOList = DataUtil.createDataList(10000);    
    long start = System.currentTimeMillis();    
    List<UserDTO> userDTOS = new ArrayList<>();    
    userDOList.forEach(userDO -> {        
    UserDTO userDTO = new UserDTO();        
    copy(userDO, userDTO);        
    userDTOS.add(userDTO);    
    });    
    log.info("BeanCopier 加緩存后 costTime: {}ms", System.currentTimeMillis() - start);}
public static void copy(Object srcObj, Object destObj) {    
    String key = genKey(srcObj.getClass(), destObj.getClass());    
    BeanCopier copier = null;    
    if (!BEAN_COPIERS.containsKey(key)) {        
      copier = BeanCopier.create(srcObj.getClass(), destObj.getClass(), false);
      BEAN_COPIERS.put(key, copier);    
    } else {        
      copier = BEAN_COPIERS.get(key);    
    }    
    copier.copy(srcObj, destObj, null);
}
private static String genKey(Class<?> srcClazz, Class<?> destClazz) {    
    return srcClazz.getName() + destClazz.getName();
}
  • 結(jié)果(耗時(shí):6ms)
20:32:31.405 [main] INFO cn.van.parameter.bean.copy.demo.BeanCopyComparedDemo 
- BeanCopier 加緩存后 costTime: 6ms

五按傅、總結(jié)及源碼

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市胧卤,隨后出現(xiàn)的幾起案子唯绍,更是在濱河造成了極大的恐慌,老刑警劉巖枝誊,帶你破解...
    沈念sama閱讀 210,978評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件况芒,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡侧啼,警方通過(guò)查閱死者的電腦和手機(jī)牛柒,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門(mén)堪簿,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)痊乾,“玉大人,你說(shuō)我怎么就攤上這事椭更∧纳螅” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,623評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀(guān)的道長(zhǎng)。 經(jīng)常有香客問(wèn)我浓镜,道長(zhǎng)开伏,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,324評(píng)論 1 282
  • 正文 為了忘掉前任臊泰,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘朝氓。我一直安慰自己,他們只是感情好主届,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,390評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布赵哲。 她就那樣靜靜地躺著,像睡著了一般君丁。 火紅的嫁衣襯著肌膚如雪枫夺。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,741評(píng)論 1 289
  • 那天绘闷,我揣著相機(jī)與錄音橡庞,去河邊找鬼较坛。 笑死,一個(gè)胖子當(dāng)著我的面吹牛扒最,可吹牛的內(nèi)容都是我干的燎潮。 我是一名探鬼主播,決...
    沈念sama閱讀 38,892評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼扼倘,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼确封!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起再菊,我...
    開(kāi)封第一講書(shū)人閱讀 37,655評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤爪喘,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后纠拔,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體秉剑,經(jīng)...
    沈念sama閱讀 44,104評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年稠诲,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了侦鹏。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,569評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡臀叙,死狀恐怖略水,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情劝萤,我是刑警寧澤渊涝,帶...
    沈念sama閱讀 34,254評(píng)論 4 328
  • 正文 年R本政府宣布,位于F島的核電站床嫌,受9級(jí)特大地震影響跨释,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜厌处,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,834評(píng)論 3 312
  • 文/蒙蒙 一鳖谈、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧阔涉,春花似錦缆娃、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,725評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至凶伙,卻和暖如春郭毕,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背函荣。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,950評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工显押, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留扳肛,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,260評(píng)論 2 360
  • 正文 我出身青樓乘碑,卻偏偏與公主長(zhǎng)得像挖息,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子兽肤,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,446評(píng)論 2 348