对象属性拷贝,到底谁更强?

smallmeteror
发布于 2023-6-13 16:13
浏览
0收藏

一、摘要

日常编程中,我们经常会碰到对象属性复制的场景,当类的属性数量只有简单的几个时,我们通过手写​​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极客技术

标签
已于2023-6-13 16:13:27修改
收藏
回复
举报
回复
    相关推荐