本文所提到的類庫已經(jīng)開源到GITHUB暇咆,讀者可下載源碼并提寶貴意見。
轉(zhuǎn)載請注明出處: http://www.reibang.com/p/9a136ecd3838
問題
在現(xiàn)有的項目中副硅,由于采用了PO/DTO/VO模型姥宝,導(dǎo)致需要大量的代碼進(jìn)行Java對象間的拷貝操作。通常的做法是取一個對象的get方法恐疲,設(shè)置到另一個對象的set方法上去:
public MaterialInfoVO getMaterialInfo(String materialId) {
MaterialInfo materialInfo = this.materialDao.findById(materialId);
MaterialInfoVO materialInfoVO = new MaterialInfoVO();
materialInfoVO.setMaterialId(materialInfo.getId());
materialInfoVO.setType(materialInfo.getType());
materialInfoVO.setArticles(new ArrayList<ArticleInfoVO>());
List<ArticleInfo> articles = materialInfo.getArticles();
for (ArticleInfo article : articles) {
ArticleInfoVO articleInfoVO = new ArticleInfoVO();
materialInfoVO.getArticles().add(articleInfoVO);
articleInfoVO.setArticleId(article.getId());
articleInfoVO.setTitle(article.getTitle());
articleInfoVO.setContent(article.getContent());
articleInfoVO.setContentSourceUrl(article.getContentSourceUrl());
articleInfoVO.setDigest(article.getDigest());
articleInfoVO.setShowCoverPic(article.getShowCoverPic());
articleInfoVO.setShowOrder(article.getShowOrder());
articleInfoVO.setThumbMediaId(article.getThumbMediaId());
articleInfoVO.setUrl(article.getUrl());
articleInfoVO.setContentSourceUrl(article.getContentSourceUrl());
}
return materialInfoVO;
}
然而腊满,大量類似重復(fù)的代碼到處可見,代碼重復(fù)度太高培己,而且很容易視覺疲勞碳蛋,導(dǎo)致字段遺漏。由此萌生了改善的想法省咨,希望能達(dá)到以下的目的:
public MaterialInfoVO getMaterialInfo(Integer materialId) {
MaterialInfo materialInfo = this.materialDao.findById(materialId);
return BeanCopyUtils.copyBean(materialInfo,MaterialInfoVO.class);
}
需求分析
為了改善代碼肃弟,能夠自動的將一個類的屬性Copy到另一個類中,并且還需要滿足以下的特定需求:
- 屬性名稱映射:From類的屬性叫id零蓉,To類的卻是materialId笤受。要能夠映射到不同名稱的屬性上去;
- 基本類型自動轉(zhuǎn)換:要能夠自動針對Java中的Primitive Type同對應(yīng)的Java Type進(jìn)行轉(zhuǎn)換敌蜂。例如:int同Integer之間轉(zhuǎn)換箩兽;
- 忽略拷貝: 要能夠支持屬性過濾,對不想進(jìn)行拷貝的屬性能夠不拷貝章喉。例如:user的password屬性就不想拷貝出去汗贫;
- 遞歸拷貝:From類中的某個屬性也是JavaBean,也需要拷貝到To類中某個JavaBean中去秸脱;
- 數(shù)組拷貝:如果From類中的某個屬性是數(shù)組類型落包,也希望能拷貝到To類中對應(yīng)的屬性中去;
- 集合拷貝:同數(shù)組拷貝一樣撞反,如果From類中的某個屬性是集合(List妥色,Set等)類型搪花,也希望能拷貝到To類中對應(yīng)的屬性中去遏片;
- 自定義數(shù)據(jù)轉(zhuǎn)換:如果在拷貝的過程中,希望能夠按照自定義的規(guī)則將某些屬性轉(zhuǎn)換成其他數(shù)據(jù)類型撮竿。例如:將Date轉(zhuǎn)換成String吮便;
- 接口簡單:我們都是程序員,不希望寫復(fù)雜的代碼幢踏。
現(xiàn)有庫調(diào)研
在目前比較通用的BeanCopy類庫中髓需,有如下的幾個類庫:
- org.apache.commons.beanutils.BeanUtil.copyProperties
- org.apache.commons.beanutils.PropertyUtils.copyProperties
- org.springframework.beans.BeanUtils.copyProperties
- org.springframework.cglib.beans.BeanCopier.create
- net.sf.ezmorph.bean.BeanMorpher
上面是目前能在市面上找到的幾個比較通用的類庫。然而房蝉,這些類庫卻很難滿足上述的需求:
Library | 屬性名稱映射 | 基本類型轉(zhuǎn)換 | 忽略拷貝 | 數(shù)組拷貝 | 集合拷貝 | 自定義轉(zhuǎn)換 | 性能 |
---|---|---|---|---|---|---|---|
apache BeanUtil. copyProperties | X | X | X | X | X | X | 慘不忍睹 |
apache PropertyUtils. copyProperties | X | X | X | X | X | X | 慘不忍睹 |
spring BeanUtils. copyProperties | X | √ | √ | X | X | X | 還能接受 |
cglib BeanCopier | X | X | X | X | X | 難用 | 優(yōu)秀 |
ezmorph BeanMorpher | X | X | X | X | X | X | 慘不忍睹 |
也許有些庫的用法還不得知僚匆,但很難全部功能都滿足微渠。看來只能自己造輪子了咧擂。
庫接口設(shè)計
函數(shù)接口
為了自己的輪子能否方便的使用逞盆,在接口函數(shù)設(shè)計上可盡量簡單。
函數(shù)調(diào)用方式如下:
// 使用class方式
ToBean toBean = BeanCopyUtils.copyBean(fromBean, ToBean.class);
// 使用對象方式
ToBean toBean = new ToBean();
BeanCopyUtils.copyBean(fromBean, toBean);
為了能達(dá)到上述的需求松申,需要引入一些注解來進(jìn)行配置云芦。
@BeanCopySource(source=FromBean.class)
public class FromBean { public class ToBean {
private boolean beanBool; private Boolean beanBool;
private byte beanByte; private Byte beanByte;
private char beanChar; private Character beanChar;
private short beanShort; private Short beanShort;
private int beanInt; private Integer beanInt;
private Long beanLong; private long beanLong;
private Float beanFloat; private float beanFloat;
private Double beanDouble; private double beanDouble;
private String beanString; private String beanString;
@CopyProperty(convertor=DateConvertor.class)
private Date beanDate; private String beanDate;
private int[] beanIntArray; private int[] beanIntArray;
@CopyProperty
private FromBean2 bean2; private ToBean2 bean2;
@CopyCollection(targetClass=ToBean3.class)
private List<FromBean3> bean3List; private List<ToBean3> bean3List;
@CopyProperty
private FromBean4[] bean4Array; private ToBean4[] bean4Array;
// getters and setters... @CopyProperty(property="beanInt")
} private int beanId;
@CopyProperty(property="bean2.beanString")
private String bean2String;
// getters and setters...
}
現(xiàn)在問題又來了,如果ToBean是第三方類庫的贸桶,無法在ToBean上加注解舅逸,怎么辦?這能難倒我嗎皇筛?這兒僅需要提供一個Option類琉历,將所有注解的內(nèi)容加到Option類中,讓Option類代替ToBean來做配置设联。
@BeanCopySource(source=FromBean.class)
public class ToBeanOption {
private Boolean beanBool;
private Byte beanByte;
private Character beanChar;
private Short beanShort;
private Integer beanInt;
private long beanLong;
private float beanFloat;
private double beanDouble;
private String beanString;
@CopyProperty(convertor=DateConvertor.class)
private String beanDate;
private int[] beanIntArray;
@CopyProperty
private ToBean2 bean2;
@CopyCollection(targetClass=ToBean3.class)
private List<ToBean3> bean3List;
@CopyProperty
private ToBean4[] bean4Array;
@CopyProperty(property="beanInt")
private int beanId;
@CopyProperty(property="bean2.beanString")
private String bean2String;
// getters and setters can be empty.
}
調(diào)用方法也非常簡單:
// 使用class方式
ToBean toBean = BeanCopyUtils.copyBean(fromBean, ToBean.class, ToBeanOption.class);
// 使用對象方式
ToBean toBean = new ToBean();
BeanCopyUtils.copyBean(fromBean, toBean, ToBeanOption.class);
是不是有點羨慕善已?
自定義數(shù)據(jù)轉(zhuǎn)換
為了支持自定義數(shù)據(jù)轉(zhuǎn)換,需要引入接口离例,以方便自定義轉(zhuǎn)換要求:
public interface BeanCopyConvertor<S, T> {
abstract public T convertTo(S object);
}
具體實現(xiàn)舉例:
public class GendorConvertor implements BeanCopyConvertor<Integer, String> {
@Override
public String convertTo(Integer object) {
if( object == 1 )
return "Male";
if( object == 2)
return "Female";
return "Unknown";
}
}
當(dāng)然换团,也需要在注解上標(biāo)明:
@CopyProperty(convertor=GendorConvertor.class)
private String gendor;
實現(xiàn)及優(yōu)化
具體的實現(xiàn)步驟不表,僅說優(yōu)化及其它考慮宫蛆。
優(yōu)化方案
- 使用緩存
由于采取的注解的方式艘包,所以在拷貝具體執(zhí)行前可以明確的知道哪些屬性要拷貝,需要做哪些映射耀盗,對應(yīng)哪些屬性要做遞歸拷貝等想虎。而且,對于特定的FromBean和ToBean以及特定的ToBeanOption叛拷,在整個Java進(jìn)程的生命期中拷貝的屬性是定死的舌厨。因此,我們可以先解析注解的拷貝信息忿薇,然后緩存起來裙椭,在后續(xù)相同的對象類型拷貝中直接使用。 - 使用Javassist類庫
如果直接使用Java的反射方式署浩,那么性能最高也就是和spring的BeanUtils.copyProperties性能相當(dāng)揉燃。對于CGlib的BeanCopier那樣高性能,只能采取相同的做法筋栋,也就是動態(tài)生成特定的Copy class炊汤,然后在執(zhí)行。可惜我對CGlib的純粹字節(jié)碼操作不感冒抢腐,因此采用了Javassist庫了姑曙。
減少依賴
類庫設(shè)計之初是為了通用使用,因此要盡量減少對其它類庫的引用迈倍。因此渣磷,除了必須要引用的javassist類庫之外,全部自我實現(xiàn)相關(guān)功能授瘦,不引用其它的類庫醋界。
更為甚者,如果要在javassist類都無法運(yùn)行的環(huán)境中使用此庫提完,也可以通過修改此類庫的全局配置形纺,切換到用反射的方式上運(yùn)行此庫。
缺省集合類
再做集合拷貝的時候徒欣,對于集合類的實現(xiàn)類可以通過全局配置的方式切換為自定義的實現(xiàn)類逐样。 例如List對應(yīng)的實現(xiàn)類,缺省為ArrayList打肝。但是如果想使用LinkedList脂新,可以通過全局配置來進(jìn)行切換。
性能
通過對不同類庫循環(huán)調(diào)用的方法來測試各個類庫的性能粗梭。
Library | 1 time | 100 times | 10000 times | 1000000 times | 10000000 times |
---|---|---|---|---|---|
apache BeanUtil.copyProperties | 1 | 12 | 128 | 9963 | 99879 |
apache PropertyUtils.copyProperties | 0 | 2 | 56 | 5564 | 55651 |
spring BeanUtils.copyProperties | 0 | 2 | 5 | 473 | 4700 |
ezmorph BeanMorpher | 1 | 4 | 67 | 6769 | 68051 |
cglib BeanCopier.create 1 | 1 | 2 | 2 | 87 | 843 |
cglib BeanCopier.create 2 | 0 | 0 | 0 | 10 | 98 |
BeanCopyUtils.copyBean 1 | 0 | 0 | 0 | 21 | 196 |
BeanCopyUtils.copyBean 2 | 0 | 0 | 0 | 11 | 97 |
native Copy | 0 | 0 | 0 | 10 | 88 |
- 表格中數(shù)字單位為毫秒争便,也就是對應(yīng)類庫運(yùn)行次數(shù)所花費的時間
- 表格中各個數(shù)據(jù)為類庫的運(yùn)行多次的平均值,運(yùn)行平臺為Win10, intel i7 6700HQ, 24G
- 具體的測試代碼也在GITHUB中断医。讀者可自行下載運(yùn)行比較
- cglib BeanCopier.create的兩種使用方法和BeanCopyUtils.copyBean的兩種使用方法滞乙,請參考GITHUB中代碼
- native copy:就是全部手動寫get/set代碼
結(jié)尾和后續(xù)
目前第一版本已經(jīng)完成,后續(xù)會加上對注解的檢查函數(shù)鉴嗤,以避免注解配置錯誤斩启。同時由于是第一版本,測試的并不是很充分醉锅,還需要大量的測試來保證正確性兔簇。
關(guān)于作者
作者很懶,很少發(fā)表技術(shù)有關(guān)的話題文章硬耍。同時作者在軟件開發(fā)上混了快20年垄琐,輾轉(zhuǎn)于C、C++默垄,Java此虑,Object-C各個開發(fā)語言之間甚纲;做過Windows口锭、Linux、Android、iOS以及Java后臺開發(fā)鹃操,管理過20+以上的團(tuán)隊韭寸。但更多的,卻是想當(dāng)一個默默奉獻(xiàn)的碼農(nóng)荆隘。