在做業(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é)
- 一旦使用Converter筒捺,BeanCopier只使用Converter定義的規(guī)則去拷貝屬性柏腻,所以在convert()方法中要考慮所有的屬性。
- 但使用Converter會(huì)使對(duì)象拷貝速度變慢系吭。
3.3 BeanCopier總結(jié)
- 當(dāng)源類(lèi)和目標(biāo)類(lèi)的屬性名稱(chēng)五嫂、類(lèi)型都相同,拷貝沒(méi)問(wèn)題肯尺。
- 當(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ì)被拷貝。
- 源類(lèi)或目標(biāo)類(lèi)的setter比getter少寨昙,拷貝沒(méi)問(wèn)題讥巡,此時(shí)setter多余,但是不會(huì)報(bào)錯(cuò)舔哪。
- 源類(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