幾款VO數(shù)據(jù)轉(zhuǎn)換工具性能剖析
前言
做后端開發(fā)的各位小伙伴應(yīng)該對數(shù)據(jù)轉(zhuǎn)換都不陌生,在實際開發(fā)中也肯定遇到過各種數(shù)據(jù)類型之間的轉(zhuǎn)換,但是在數(shù)據(jù)轉(zhuǎn)換的時候,往往是需要考慮性能問題的,特別是在大批量數(shù)據(jù)轉(zhuǎn)換的時候更是如此,所以本著性能為王的原則,我們今天來評測幾種數(shù)據(jù)轉(zhuǎn)換方案,主要包括以下幾種:
getter/setter手動賦值BeanUtilsBeanMap- 反射
我們先說結(jié)論,這幾種方式性能排行依次是:getter/setter > 反射 > BeanMap > BeanUtil
數(shù)據(jù)轉(zhuǎn)換
因為參與公司的重構(gòu)項目,前段時間一直比較忙,工作重點主要是業(yè)務(wù)代碼編寫測試,最近稍微好一點,主要在做一些優(yōu)化方面的工作,由于本次項目重構(gòu)主要是為了解決性能問題,所以這次重構(gòu)對性能的要求也稍微高一點,昨天在優(yōu)化數(shù)據(jù)轉(zhuǎn)換方案的時候,在自己的探索和不屑努力之下,終于找到了另外一種性能比較好的數(shù)據(jù)轉(zhuǎn)換方案,所以今天我們的內(nèi)容就是這幾種數(shù)據(jù)轉(zhuǎn)換方案的對比。
我們先看下兩個需要進行轉(zhuǎn)換的VO,首先是entity,類似于數(shù)據(jù)庫查出來的數(shù)據(jù):
public?class?UserEntity?{
????/**
?????*?用戶?id
?????*/
????private?Long?id;
????/**
?????*?用戶名
?????*/
????private?String?userName;
????/**
?????*?昵稱
?????*/
????private?String?nickName;
???//?省略構(gòu)造方法和getter/setter方法
??}
然后是另一個,類似于需要給前端返回的數(shù)據(jù):
public?class?UserVo?{
????/**
?????*?用戶?id
?????*/
????private?Long?id;
????/**
?????*?用戶名
?????*/
????private?String?userName;
????/**
?????*?昵稱
?????*/
????private?String?nickName;
????//?省略構(gòu)造方法和getter/setter方法
}
數(shù)據(jù)初始化:
?//?數(shù)據(jù)初始化
List?userEntityList?=?Lists.newArrayList();
for?(int?i?=?0;?i?10000;?i++)?{
????UserEntity?userEntity?=?new?UserEntity(123L?+?i,?"syske",?"云中志");
????userEntityList.add(userEntity);
}
為了方便測試,這里我先初始10000條數(shù)據(jù)。
getter/setter
這種方式是最直接、性能最好的方式,當(dāng)然也是兼容性最差的方式,因為這種方式兩個對象需要強依賴,如果字段增加需要修改轉(zhuǎn)換方法,對改動不友好:
long?start1?=?System.currentTimeMillis();
List?userVoList?=?Lists.newArrayList();
for?(UserEntity?userEntity?:?userEntityList)?{
????UserVo?userVo?=?new?UserVo();
????userVo.setId(userEntity.getId());
????userVo.setUserName(userEntity.getUserName());
????userVo.setNickName(userEntity.getNickName());
????userVoList.add(userVo);
}
System.out.printf("getter/setter耗時:%s\n",?System.currentTimeMillis()?-?start1);
BeanUtils
BeanUtils是spring-beans包下面的一個工具了,為我們提供了豐富的方法,這里我們主要測試的是copyProperties方法,這個方法有兩個參數(shù),第一個參數(shù)是源數(shù)據(jù),第二個是需要賦值的目標(biāo)數(shù)據(jù):
?long?start2?=?System.currentTimeMillis();
?List?userVoList2?=?Lists.newArrayList();
?for?(UserEntity?userEntity?:?userEntityList)?{
?UserVo?userVo?=?new?UserVo();
?BeanUtils.copyProperties(userEntity,?userVo);
?userVoList2.add(userVo);
?}
?System.out.printf("BeanUtils耗時:%s\n",?System.currentTimeMillis()?-?start2);
這種方式就比較簡潔了,簡單來說就是對兩個對象進行屬性值拷貝,但是屬性必須要有setter方法(被賦值方)和getter方法(源數(shù)據(jù)),copyProperties方法提供了忽略屬性值的方式,除此外暫未發(fā)現(xiàn)有任何優(yōu)點。如果只是單個對象的轉(zhuǎn)換,而且不考慮性能的話,這種方式也比較方便。(起初我以為它至少可以提供無getter/setter方法的值拷貝,但是測試之后我發(fā)現(xiàn)我想多了,底層還是反射)。
BeanMap
BeanMap也是spring-beans包下面的,不過它屬于cglib包(一個強大的,高性能,高質(zhì)量的Code生成類庫),它主要提供了一種Bean轉(zhuǎn)Map的能力,最初我也是因為這個接觸這個包的,最后發(fā)現(xiàn)它還可以用來做類型轉(zhuǎn)換,而且性能也還不錯。通過BeanMap來實現(xiàn)類型轉(zhuǎn)換的思路也很簡單,就是分別將目標(biāo)vo和源vo分別轉(zhuǎn)為BeanMap,然后用源Vo的BeanMap覆蓋目標(biāo)vo的BeanMap,然后通過BeanMap的getBean方法從BeanMap中拿到賦值后的vo,下面是具體實現(xiàn):
?long?start3?=?System.currentTimeMillis();
List?userVoList3?=?Lists.newArrayList();
for?(UserEntity?userEntity?:?userEntityList)?{
????UserVo?userVo?=?new?UserVo();
????BeanMap?entityMap?=?BeanMap.create(userEntity);
????BeanMap?userVOMap?=?BeanMap.create(userVo);
????userVOMap.putAll(entityMap);
????userVoList3.add((UserVo)?userVOMap.getBean());
}
System.out.printf("BeanMap耗時:%s\n",?System.currentTimeMillis()?-?start3);
這種方式的好處也是簡潔,但是性能比BeanUtils好,和getter/setter比更靈活,缺點是不能忽略值,但是可以自己實現(xiàn),也不難。
反射
反射這種方式也算比較原始的解耦方式,缺點是稍微有點繁瑣,但是優(yōu)勢是性能比BeanUtils和BeanMap要好。
long?start4?=?System.currentTimeMillis();
List?userVoList4?=?Lists.newArrayList();
for?(UserEntity?userEntity?:?userEntityList)?{
????UserVo?userVo?=?new?UserVo();
????Class?extends?UserEntity>?eClass?=?userEntity.getClass();
????Class?extends?UserVo>?vClass?=?userVo.getClass();
????Field[]?fields?=?eClass.getDeclaredFields();
????Field[]?vClassDeclaredFields?=?vClass.getDeclaredFields();
????List?fieldList?=??Lists.newArrayList(vClassDeclaredFields);
????for?(Field?field?:?fields)?{
????????if?(fieldList.contains(field))?{
????????????String?name?=?field.getName().substring(0,?1).toUpperCase()?+?field.getName().substring(1);
????????????Method?setter?=?vClass.getMethod("set"?+?name,?field.getType());
????????????Method?getter?=?eClass.getMethod("get"?+?name,?null);
????????????setter.invoke(userVo,?getter.invoke(userEntity,?null));
????????}
????}
????userVoList4.add(userVo);
}
System.out.printf("反射耗時:%s",?System.currentTimeMillis()?-?start4);
但是這種方式的缺點是如果兩個vo屬性不一致時需要單獨處理,我們可以看到代碼中有屬性字段的校驗。
性能對比
下面我們就分別針對不同的數(shù)據(jù)量做一個簡單的測試,對比下各種方案的性能,首先是三個字段在不同數(shù)據(jù)量下的性能比較:

三個字段進行數(shù)據(jù)轉(zhuǎn)換時,我們可以得出以下結(jié)論:
- 在十萬條數(shù)據(jù)的數(shù)據(jù)之前,
getter/setter性能變化不大,一直表現(xiàn)很優(yōu)秀,大概是BeanUtils的30倍; BeanUntils和BeanMap差距也不是特別大,差距最大也就兩倍左右;反射和getter/setter的性能差異大概是3倍;- 綜合極值來看,
getter/setter的性能差異大概是50倍,性能急劇變化發(fā)生在數(shù)據(jù)由10萬變?yōu)?code style="background-color:rgba(27,31,35,.05);font-family:'Operator Mono', Consolas, Monaco, Menlo, monospace;color:rgb(255,100,65);">100萬的時候;反射的性能差異差不多是90倍,性能是從10000條的時候發(fā)生變化的;BeanMap和BeanUtils性能變化不到,差不多7倍左右
下面我們再看下字段數(shù)量增多的情況:

相比于3個字段,12個字段的性能并沒有發(fā)生太大改變,變化比較大的是反射這種方式,其他三種方式并沒有太大變化,甚至還出現(xiàn)性能更好的情況,但是再100萬數(shù)據(jù)量的時候,反射性能比BeanMap差,不過也能想明白,畢竟字段越多,反射需要循環(huán)的次數(shù)就越多,所以性能會下降。好了,關(guān)于測試我們就到這里吧。
結(jié)語
介于時間的關(guān)系,我們今天的內(nèi)容就先到這里,感興趣的小伙伴可以自己測試下,總體來說,我們的預(yù)期目標(biāo)算是達成了。最后,希望通過今天的性能測試,能夠讓各位小伙伴重視開發(fā)過程中的性能問題,找到更適合的方案。好了,各位小伙伴,晚安吧!
- END -