對象屬性拷貝,到底誰更強?
一、摘要
日常編程中,我們經常會碰到對象屬性復制的場景,當類的屬性數量只有簡單的幾個時,我們通過手寫set/get即可完成,但是屬性有十幾個,甚至幾十個的時候,通過set/get的方式,可能會占用大量的編程時間,關鍵是像這樣的代碼,基本上是機械式的操作。
面對這種重復又枯燥的編程工作,很多的行業大佬,開發出了通用的對象屬性復制工具,以免去機械式的編程。
小編經過實際的調研,發現目前開源市場上,用得比較多的對象屬性復制工具有以下幾個:
- Apache BeanUtils
- Spring BeanUtils
- Cglib BeanCopier
- MapStruct
下面我們一起來看看,他們的使用方式以及性能對比,最后根據不同的使用需求,總結出如何選擇。
二、工具實踐
首先我們定義一個UserInfo類,下面我們會以此類作為對象屬性復制的案例,內容如下:
public class UserInfo {
/**
* 用戶ID
*/
private Long userId;
/**
* 用戶名
*/
private String userName;
/**
* 密碼
*/
private String userPwd;
/**
* 年齡
*/
private Integer age;
/**
* 性別
*/
private String gender;
/**
* 生日
*/
private Date birthday;
// ...set、get
@Override
public String toString() {
return "UserInfo{" +
"userId=" + userId +
", userName='" + userName + '\'' +
", userPwd='" + userPwd + '\'' +
", age=" + age +
", gender='" + gender + '\'' +
", birthday=" + birthday +
'}';
}
}
2.1、Apache BeanUtils
Apache BeanUtils 這個工具,相信很多人都接觸過,因為它在 Java 領域屬性復制這塊非常有名,早期使用的非常廣泛!
使用方式上也非常的簡單,首先在項目中導入commons-beanutils包。
<!--Apache BeanUtils-->
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.9.4</version>
</dependency>
然后在代碼中直接導入org.apache.commons.beanutils.BeanUtils工具進行對象屬性復制,樣例代碼如下:
// 原始對象
UserInfo source = new UserInfo();
// set...
// 目標對象
UserInfo target = new UserInfo();
BeanUtils.copyProperties(target, source);
System.out.println(target.toString());
Apache BeanUtils 工具從操作使用上還是非常方便的,不過其底層源碼為了追求完美,加了過多的包裝,使用了很多反射,做了很多校驗,導致屬性復制時性能較差,因此阿里巴巴開發手冊上強制規定避免使用 Apache BeanUtils。
關于對象屬性復制性能,我們會文末向大家介紹!
2.2、Spring BeanUtils
Spring 對象屬性復制工具類,類名與 Apache 一樣,基本用法也差不多。
首先在項目中導入spring-beans包。
<!--spring BeanUtils-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>4.3.30.RELEASE</version>
</dependency>
同樣的,在代碼中直接導入org.springframework.beans.BeanUtils工具進行對象屬性復制,樣例代碼如下:
// 原始對象
UserInfo source = new UserInfo();
// set...
// 目標對象
UserInfo target = new UserInfo();
BeanUtils.copyProperties(source, target);
System.out.println(target.toString());
除此之外,Spring BeanUtils 還提供了重載方法。
public static void copyProperties(Object source, Object target, String... ignoreProperties);
如果我們不想對象中的某些屬性被復制過去,可以通過如下方式實現。
BeanUtils.copyProperties(source, target, "userPwd");
雖然Apache BeanUtils和Spring BeanUtils使用起來都很方便,但是兩者性能差異非常大,Spring BeanUtils的對象屬性復制速度比Apache BeanUtils要快很多,主要原因在于 Spring 并沒有像 Apache 一樣使用反射做過多的參數校驗,另外Spring BeanUtils內部使用了緩存,加快了轉換的速度。
還有一個需要注意的地方是,Apache BeanUtils和Spring BeanUtils的類名和方法基本上相同,但是它們的原始對象和目標對象的參數位置是相反的,如果直接從Apache BeanUtils切換到Spring BeanUtils有巨大的風險,如果有這個方面的需要,請一個一個的替換!
2.3、Cglib BeanCopier
Cglib BeanCopier 對象屬性復制工具類,相比Spring BeanUtils和Apache BeanUtils,用法稍微要多一步代碼。
首先在項目中導入cglib包。
<!--cglib-->
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>
然后在代碼中直接導入net.sf.cglib.beans.BeanCopier工具進行對象屬性復制,樣例代碼如下:
// 原始對象
UserInfo source = new UserInfo();
// set...
// 獲取一個復制工具
BeanCopier beanCopier = BeanCopier.create(UserInfo.class, UserInfo.class, false);
// 對象屬性值復制
UserInfo target = new UserInfo();
beanCopier.copy(source, target, null);
System.out.println(target.toString());
如果遇到字段名相同,但是類型不一致的對象復制,可以引入轉換器,進行類型轉換,比如這樣:
UserInfo source = new UserInfo();
// set...
// 創建一個復制工具
BeanCopier beanCopier = BeanCopier.create(UserInfo.class, UserInfo.class, true);
// 自定義對象屬性值復制
UserInfo target = new UserInfo();
beanCopier.copy(source, target, new Converter() {
@Override
public Object convert(Object source, Class target, Object context) {
if(source instanceof Integer){
return String.valueOf(source);
}
return source;
}
});
System.out.println(target.toString());
Cglib BeanCopier 的工作原理與上面兩個Beanutils原理不太一樣,其主要使用字節碼技術動態生成一個代理類,通過代理類來實現get/set方法。
雖然生成代理類過程存在一定開銷,但是一旦生成可以重復使用,因此 Cglib 性能相比以上兩種 Beanutils 性能都要好。
另外就是,如果你的工程是基于 Spring 框架開發的,查找 BeanCopier 這個類的時候,可以發現兩個不同的包,一個屬于Cglib,另一個屬于Spring-Core。
其實Spring-Core內置的BeanCopier引入了 Cglib 中的類,這么做的目的是為保證 Spring 中使用 Cglib 相關類的穩定性,防止外部 Cglib 依賴不一致,導致 Spring 運行異常,因此無論你引用那個包,本質都是使用 Cglib。
2.4、MapStruct
MapStruct 也是一款對象屬性復制的工具,但是它跟我們上面介紹的幾款工具技術實現思路都不一樣,主要區別在于:無論是Beanutils還是BeanCopier,都是程序運行期間去執行對象屬性復制操作;而MapStruct是在程序編譯期間,就已經生成好了對象屬性復制相關的邏輯。
因此可以想象的到,MapStruct的復制性能要快很多!
MapStruct工具的使用也很簡單,首先導入相關包。
<!--mapstruct 核心庫-->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.5.0.Final</version>
</dependency>
<!--mapstruct 編譯器插件-->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.5.0.Final</version>
<scope>provided</scope>
</dependency>
然后定義一個對象屬性拷貝接口。
@Mapper
public interface UserInfoMapper {
UserInfoMapper INSTANCE = Mappers.getMapper(UserInfoMapper.class);
UserInfo copy(UserInfo source);
}
最后,在代碼中調用即可。
// 原始對象
UserInfo source = new UserInfo();
// set...
// 對象屬性復制
UserInfo target = UserInfoMapper.INSTANCE.copy(source);
System.out.println(target.toString());
MapStruct實現原理,剛剛也介紹了,是在編譯期間生成對象屬性復制相關的代碼邏輯,以上面的操作為例,打開class目錄文件,你會看到一個UserInfoMapperImpl實現類,已經幫我們做好了set/get操作,源碼內容如下:
public class UserInfoMapperImpl implements UserInfoMapper {
public UserInfoMapperImpl() {
}
public UserInfo copy(UserInfo source) {
if (source == null) {
return null;
} else {
UserInfo userInfo = new UserInfo();
userInfo.setUserId(source.getUserId());
userInfo.setUserName(source.getUserName());
userInfo.setUserPwd(source.getUserPwd());
userInfo.setAge(source.getAge());
userInfo.setGender(source.getGender());
userInfo.setBirthday(source.getBirthday());
return userInfo;
}
}
}
三、性能對比
了解完以上的對象屬性復制工具之后,回到我們最初發出的疑問,到底誰最強呢?
下面我們通過循環執行對象屬性復制次數,分別測試set/get、Apache BeanUtils、Spring BeanUtils、Cglib BeanCopier、MapStruct等執行方法,看看它們所消耗的時間如何,統計報表明細如下!
從圖中我們可以得出如下結論!
- 1.set/get方式操作最簡單,性能最強,當之無愧 number one!但是機械式編程工作比較多。
- 2.MapStruct方式性能其次,主要的優勢在于編譯期間生成set/get代碼,但是操作不夠靈活,如果需要復制的目標對象很多,需要定義多個接口或者方法。
- 3.Cglib BeanCopier和Spring BeanUtils,雖然兩者使用的技術方案各有不同,在實際的測試過程中,100 萬數據量以下的循環復制操作差異并不明顯,編程方面比較明顯的區別是BeanCopier比BeanUtils稍微多一行代碼,日常使用中,兩者都可以,根據自己的喜好選擇即可。
- 4.Apache BeanUtils和Spring BeanUtils,兩者的底層都是基于類反射進行屬性復制,不同的地方在于Apache BeanUtils加了過多的包裝,使用了很多反射,做了很多校驗,導致性能大大削弱了,同時Spring BeanUtils內部使用了緩存,加快了轉換的速度,因此如果要二選一,推薦采用Spring BeanUtils工具。
四、小結
本文主要圍繞對象屬性復制,從使用方面做了一次簡單的內容總結。
通過以上 5 種方式的對象屬性復制操作,給出的建議如下:
- 1.如果當前類只有簡單的幾個屬性,建議直接使用set/get,原生編程性能最好
- 2.如果類的屬性很多,可以使用Spring BeanUtils或者Cglib BeanCopier工具,可以省下很多的機械式編程工作
- 3.如果當前類屬性很多,同時對復制性能有要求,推薦使用MapStruct
最后,以上的對象屬性復制工具都是淺拷貝的實現方式,如果要深拷貝,可以使用對象序列戶和反序列化技術實現!