聽說你還在使用 BeanUtils 來 copy 屬性?來試試這個(gè)吧
在《阿里巴巴 Java 開發(fā)規(guī)范》手冊最后一節(jié)的應(yīng)用分層中推薦了應(yīng)用系統(tǒng)的分層結(jié)構(gòu)。
我比較贊同這種分層結(jié)構(gòu),這種分層結(jié)構(gòu)帶來了諸多好處,但是有一個(gè)麻煩之處就是分層領(lǐng)域模型,也就是我們所說的各種 O,比如 DTO、POJO、DO、VO 等等,這樣就導(dǎo)致我們項(xiàng)目中存在各種屬性相同的 xxO,對于有些工作經(jīng)驗(yàn)的小伙伴們來說知道使用 BeanUtils 來實(shí)現(xiàn)屬性復(fù)制,但是對于工作經(jīng)驗(yàn)不是很多的小伙伴可能就是各種 set 和 get 了。這是非常尷尬的一件事。
Spring 的 BeanUtils 雖然可以滿足我們大部分的需要,但是只能賦值屬性名相同且類型一致的兩個(gè)屬性,比如 VO 里面的 beginTime 是 String 類型的,而 BO 里面的 beginTime 是 Date 類型就無法賦值了。怎么解決這種問題呢?使用 Orika。
Orika 它簡化了不同層對象之間映射過程。使用字節(jié)碼生成器創(chuàng)建開銷最小的快速映射,比其他基于反射方式實(shí)現(xiàn)(如,Dozer)更快。
簡單示例
-
先定義兩個(gè)需要轉(zhuǎn)換的 VO 和 BO
public class UserVO {
private String userName;
private Integer userAge;
}
public class UserBO {
private String userName;
private Integer userAge;
}
-
測試
Orika 的基礎(chǔ)類是 MapperFactory,其用于配置映射并獲得用于執(zhí)行映射工作的 MapperFacade 實(shí)例,如下:
MapperFactory mapperFactory = new DefaultMapperFactory.Builder().build();
我們將 UserVO 當(dāng)做源數(shù)據(jù),將 UserBO 當(dāng)做目標(biāo)數(shù)據(jù),如下:
public static void main(String[] args){
MapperFactory mapperFactory = new DefaultMapperFactory.Builder().build();
mapperFactory.classMap(UserVO.class,UserBO.class);
MapperFacade mapperFacade = mapperFactory.getMapperFacade();
UserVO userVO = new UserVO("chenssy",18);
UserBO userBO = mapperFacade.map(userVO,UserBO.class);
System.out.println("userName:" + userBO.getUserName() + " --- userAge:" + userBO.getUserAge() );
}
運(yùn)行結(jié)果:
userName:chenssy --- userAge:18
這只是一個(gè)比較簡單的示例,到這里,可能有小伙伴說:使用 Spring 的 BeanUtils 也可以實(shí)現(xiàn),而且代碼量更加少,更加簡單,那下面小編就演示他的高級功能,看 Spring BeantUtils 是否還能夠?qū)崿F(xiàn)。
使用
使用 Orika 需要添加 maven 映射:
<dependency>
<groupId>ma.glasnost.orika</groupId>
<artifactId>orika-core</artifactId>
<version>1.4.6</version>
</dependency>
字段不相同映射
上面的示例源對象和目標(biāo)對象兩者的屬性都一致,如果兩者的屬性不一致該如何處理呢?如下:
public class UserVO {
private String userName;
private Integer userAge;
}
public class UserBO {
private String name;
private Integer userAge;
}
我們需要將 userName 的值賦值給 name,userAge 的值賦值給 age,字段映射如下:
public static void main(String[] args){
MapperFactory mapperFactory = new DefaultMapperFactory.Builder().build();
mapperFactory.classMap(UserVO.class, UserBO.class)
.field("userName","name")
.field("userAge","userAge").register();
MapperFacade mapperFacade = mapperFactory.getMapperFacade();
UserVO userVO = new UserVO("chenssy-2",19);
UserBO userBO = mapperFacade.map(userVO,UserBO.class);
System.out.println("userName:" + userBO.getName() + " --- userAge:" + userBO.getUserAge() );
}
運(yùn)行結(jié)果如下:
userName:chenssy-2 --- userAge:19
在這里需要注意的是:在進(jìn)行字段映射時(shí)不能忘記調(diào)用 register() 方法,它是為了給 MapperFactory 注冊配置信息的。
如果按照上面的使用方法,則需要在注冊屬性映射時(shí)要注冊所有的屬性,哪怕只有一個(gè)屬性不一致也要注冊所有字段映射,包括相同的字段。這種方式會(huì)讓人崩潰的,比如有 20 個(gè)屬性只要 1 個(gè)不同,難道也需要配置其余 19 個(gè)相同的屬性?當(dāng)然不,我們可以通過設(shè)置缺省映射配置,這樣就無效顯示定義映射的。如下:
mapperFactory.classMap(UserVO.class, UserBO.class)
.field("userName","name")
.byDefault().register();
一樣可以得到上面相同的運(yùn)行結(jié)果。
排除字段
在我們實(shí)際工作中,對于一個(gè) DO 賦值,我們可能只需要其中某一些字段,對于其他的字段我們需要排除掉,這個(gè)時(shí)候,我們就可以使用 exclude() 不希望該字段參與映射。比如上面實(shí)例的 userAge。如下:
mapperFactory.classMap(UserVO.class, UserBO.class)
.field("userName","name")
.exclude("userAge")
.byDefault().register();
運(yùn)行結(jié)果如下:
userName:chenssy-2 --- userAge:null
集合映射
List 集合
將 UserVO 集合數(shù)據(jù)拷貝到 UserBO 集合中。一般這種情況在我們實(shí)際工作中是非常多見的。
不多說,直接上代碼。
public static void main(String[] args){
MapperFactory mapperFactory = new DefaultMapperFactory.Builder().build();
MapperFacade mapperFacade = mapperFactory.getMapperFacade();
mapperFactory.classMap(UserVO.class,UserBO.class);
List<UserVO> userList = new ArrayList<>();
userList.add(new UserVO("chenssy_1",18));
userList.add(new UserVO("chenssy_2",19));
userList.add(new UserVO("chenssy_3",20));
//進(jìn)行集合復(fù)制
List<UserBO> userBoList = mapperFacade.mapAsList(userList,UserBO.class);
for(UserBO userBO : userBoList){
System.out.println("userName:" + userBO.getUserName() + " --- userAge:" + userBO.getUserAge() );
}
}
運(yùn)行結(jié)果:
userName:chenssy_1 --- userAge:18
userName:chenssy_2 --- userAge:19
userName:chenssy_3 --- userAge:20
其實(shí)代碼與簡單示例中的代碼沒什么區(qū)別,僅僅只是將 map() 方法替換成了 mapAsList() 方法,至于 Map、Set,則 Orika 都提供了相應(yīng)的方法可以進(jìn)行映射,這里就不多介紹了。
VO 中有集合
我們可能會(huì)遇到這樣一種情況,那就是 VO 中包含著一個(gè)或者多個(gè)集合屬性,我們需要將他們的值拷貝到另一個(gè) BO 中的集合屬性中。如下:
public class UserBOList {
private List<UserBO> list;
}
public class UserVOList {
private List<UserVO> list;
}
UserBOList 和 UserVOList 中包含一個(gè) List,分別是 UserBO 和 UserVO 的集合。
public static void main(String[] args){
MapperFactory mapperFactory = new DefaultMapperFactory.Builder().build();
// 注意這里是 List 的集合中的對象數(shù)據(jù)
mapperFactory.classMap(UserVO.class,UserBO.class)
.field("userName","name")
.field("userAge","userAge")
.byDefault()
.register();
List<UserVO> list = new ArrayList<>();
list.add(new UserVO("chenssy_11",118));
list.add(new UserVO("chenssy_22",228));
list.add(new UserVO("chenssy_33",338));
UserVOList userVOList = new UserVOList();
userVOList.setList(list);
UserBOList userBOList = mapperFactory.getMapperFacade().map(userVOList,UserBOList.class);
for(UserBO userBO : userBOList.getList()){
System.out.println("userName:" + userBO.getName() + " --- userAge:" + userBO.getUserAge() );
}
}
運(yùn)行結(jié)果:
userName:chenssy_11 --- userAge:118
userName:chenssy_22 --- userAge:228
userName:chenssy_33 --- userAge:338
Map 轉(zhuǎn)換
Map 轉(zhuǎn)換 Bean 是非常常見的常見,下面就演示下,如何利用 Orika 完成 Map 到 Bean 的轉(zhuǎn)換過程。
public static void main(String[] args){
Map<String,Object> map = new HashMap<>();
map.put("userName","chenssy1");
map.put("userAge",18);
MapperFactory mapperFactory = new DefaultMapperFactory.Builder().build();
mapperFactory.classMap(HashMap.class, UserBO.class)
.field("userName","userName")
.field("userAge","userAge")
.byDefault()
.register();
UserBO userBO = mapperFactory.getMapperFacade().map(map,UserBO.class);
System.out.println("userName:" + userBO.getUserName() + " --- userAge:" + userBO.getUserAge() );
}
就是如此的簡單。
嵌套字段映射
加入在源數(shù)據(jù)對象中,有另外一個(gè) DTO 需要保持我們映射的值。
這種場景還是挺多見的,不多說,直接看代碼。
public class Person {
private UserVO userVO;
}
Person 中包含 UserVO 實(shí)例。那如何將其映射到目標(biāo)對象中呢?為了訪問嵌套 DTO 的屬性并映射到目標(biāo)對象,我們只需要使用 . 即可,如下:
public static void main(String[] args){
MapperFactory mapperFactory = new DefaultMapperFactory.Builder().build();
mapperFactory.classMap(Person.class, UserBO.class)
.field("userVO.userName","name")
.field("userVO.userAge","userAge")
.register();
UserVO userVO = new UserVO("chenssy",18);
Person person = new Person(userVO);
UserBO userBO = mapperFactory.getMapperFacade().map(person,UserBO.class);
System.out.println("userName:" + userBO.getName() + " --- userAge:" + userBO.getUserAge() );
}
運(yùn)行結(jié)果:
userName:chenssy --- userAge:18
Bean 映射工具選擇
以下內(nèi)容摘自:https://www.jianshu.com/p/40e0e64797b9
-
BeanUtils
Apache的BeanUtils和spring的BeanUtils中拷貝方法的原理都是先用jdk中 java.beans.Introspector類的getBeanInfo()方法獲取對象的屬性信息及屬性get/set方法,接著使用反射(Method的invoke(Object obj, Object... args))方法進(jìn)行賦值。Apache支持名稱相同但類型不同的屬性的轉(zhuǎn)換,spring支持忽略某些屬性不進(jìn)行映射,他們都設(shè)置了緩存保存已解析過的BeanInfo信息。
-
BeanCopier
cglib的BeanCopier采用了不同的方法:它不是利用反射對屬性進(jìn)行賦值,而是直接使用ASM的MethodVisitor直接編寫各屬性的get/set方法(具體過程可見BeanCopier類的generateClass(ClassVisitor v)方法)生成class文件,然后進(jìn)行執(zhí)行。由于是直接生成字節(jié)碼執(zhí)行,所以BeanCopier的性能較采用反射的BeanUtils有較大提高,這一點(diǎn)可在后面的測試中看出。
-
Dozer
使用以上類庫雖然可以不用手動(dòng)編寫get/set方法,但是他們都不能對不同名稱的對象屬性進(jìn)行映射。在定制化的屬性映射方面做得比較好的有Dozer,Dozer支持簡單屬性映射、復(fù)雜類型映射、雙向映射、隱式映射以及遞歸映射。可使用xml或者注解進(jìn)行映射的配置,支持自動(dòng)類型轉(zhuǎn)換,使用方便。但Dozer底層是使用reflect包下Field類的set(Object obj, Object value)方法進(jìn)行屬性賦值,執(zhí)行速度上不是那么理想。
-
Orika
那么有沒有特性豐富,速度又快的Bean映射工具呢.,Orika是近期在github活躍的項(xiàng)目,底層采用了javassist類庫生成Bean映射的字節(jié)碼,之后直接加載執(zhí)行生成的字節(jié)碼文件,因此在速度上比使用反射進(jìn)行賦值會(huì)快很多。
