对象属性拷贝,到底谁更强?
一、摘要
日常编程中,我们经常会碰到对象属性复制的场景,当类的属性数量只有简单的几个时,我们通过手写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
最后,以上的对象属性复制工具都是浅拷贝的实现方式,如果要深拷贝,可以使用对象序列户和反序列化技术实现!
关于对象深拷贝,我们会在后续的文章中给大家介绍,本文内容难免有描述不对的地方,欢迎网友留言指出!
文章转载自公众号:Java极客技术