Home Tags Posts tagged with "BeanCopier"

BeanCopier

0 78

常见的对象属性拷贝方式解析

一、常见的对象属拷贝方式

  1. Apache BeanUtils

  2. Apache ProrertyUtils

  3. Springframework BeanUtils

  4. Cglib BeanCopier

二、性能对比

private static void testCglibBeanCopier(OriginObject origin, int len) {
        Stopwatch stopwatch = Stopwatch.createStarted();
        System.out.println();
        System.out.println("================cglib BeanCopier执行" + len + "次================");
        DestinationObject destination3 = new DestinationObject();

        for (int i = 0; i < len; i++) {
            BeanCopier copier = BeanCopier.create(OriginObject.class, DestinationObject.class, false);
            copier.copy(origin, destination3, null);
        }
        stopwatch.stop();

        System.out.println("testCglibBeanCopier 耗时: " + stopwatch.elapsed(TimeUnit.MILLISECONDS));
    }

    private static void testApacheBeanUtils(OriginObject origin, int len)
            throws IllegalAccessException, InvocationTargetException {
        Stopwatch stopwatch = Stopwatch.createStarted();
        System.out.println();
        System.out.println("================apache BeanUtils执行" + len + "次================");
        DestinationObject destination2 = new DestinationObject();
        for (int i = 0; i < len; i++) {
            BeanUtils.copyProperties(destination2, origin);
        }

        stopwatch.stop();

        System.out.println("testApacheBeanUtils 耗时: " + stopwatch.elapsed(TimeUnit.MILLISECONDS));
    }

    private static void testSpringFramework(OriginObject origin, int len) {
        Stopwatch stopwatch = Stopwatch.createStarted();
        System.out.println("================springframework执行" + len + "次================");
        DestinationObject destination = new DestinationObject();

        for (int i = 0; i < len; i++) {
            org.springframework.beans.BeanUtils.copyProperties(origin, destination);
        }

        stopwatch.stop();

        System.out.println("testSpringFramework 耗时: " + stopwatch.elapsed(TimeUnit.MILLISECONDS));
    }

    private static void testApacheBeanUtilsPropertyUtils(OriginObject origin, int len)
            throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
        Stopwatch stopwatch = Stopwatch.createStarted();
        System.out.println();
        System.out.println("================apache BeanUtils PropertyUtils执行" + len + "次================");
        DestinationObject destination2 = new DestinationObject();
        for (int i = 0; i < len; i++) {
            PropertyUtils.copyProperties(destination2, origin);
        }

        stopwatch.stop();

        System.out.println("testApacheBeanUtilsPropertyUtils 耗时: " + stopwatch.elapsed(TimeUnit.MILLISECONDS));
    }

分别执行1000、10000、100000、1000000次耗时数(毫秒):

工具名称 执行1000次耗时 10000次 100000次 1000000次
Apache BeanUtils 390ms 854ms 1763ms 8408ms
Apache PropertyUtils 26ms 221ms 352ms 2663ms
spring BeanUtils 39ms 315ms 373ms 949ms
Cglib BeanCopier 64ms 144ms 171ms 309ms

性能对比结论

  1. Apache BeanUtils的性能最差,不建议使用。

  2. Apache PropertyUtils100000次以内性能还能接受,到百万级别性能就比较差了,可酌情考虑。

  3. spring BeanUtils和BeanCopier性能较好,如果对性能有特别要求,可使用BeanCopier,不然spring BeanUtils也是可取的。

  4. Cglib 的 BeanCopier 的拷贝速度是最快的,即使是百万次的拷贝也只需要 10 毫秒! 相比而言,最差的是 Commons 包的 BeanUtils.copyProperties 方法,100 次拷贝测试与表现最好的 Cglib 相差400 倍之多。百万次拷贝更是出现了2600 倍的性能差异!

    综上: 性能 setter > cglib > spring > apache PropertyUtils > apache BeanUtils

三、源码解析

此处只解析Apache BeanUtils 源码

解析源码

首先,抛出结论,Apache BeanUtils.copyProperties()性能差的原因是:

  1. 输出了大量的日志调试信息

  2. 重复的对象类型检查

开始分析

执行1000000次copy属性,然后通过jvisualvm查看方法耗时,如图:

img

发现最耗时的方法就是method.invoke(),但是spring的BeanUtils、PropertyUtils里也是采用反射来实现的,为什么效率相差这么大呢?img

看来Abstract.convert()和getIntrospectionData()占用了很大一部分时间.而且Apache BeanUtils中的日志输出也比较耗时。

与 Spring BeanUtils 进行对比

img

PropertyUtils和Apache BeanUtils核心代码区别在图中标注的地方。

Apache BeanUtils主要集中了各种丰富的功能(日志、转换、解析等等),导致性能变差。

而Spring BeanUtils则是直接通过反射来读取和写入,直抒胸臆,省去了其他繁杂的步骤,性能自然不差。

四、使用总结(重点)

对 Apache BeanUtils 的总结

  1. 拷贝性能 setter > Cglib BeanCopier > Spring BeanUtils > Apache PropertyUtils > Apache BeanUtils

  2. Apache BeanUtils.copyProperties()性能差的原因是对日志的大量操作和重复的对象类型检查

  3. Apache BeanUtils.copyProperties()坑点

    • 包装类默认值

    在进行属性拷贝时,低版本 BeanUtils 为了解决Date为空的问题会导致为目标对象的**原始类型的包装类属性赋予初始值**,如 Integer 属性默认赋值为 0,尽管你的来源对象该字段的值为 null。这个在我们的**包装类属性为 null 值时有特殊含义的场景**,非常容易踩坑!例如搜索条件对象,一般 null 值表示该字段不做限制,而 0 表示该字段的值必须为0。
    • 改用 Spring BeanUtils 时,要特别小心

    org.apache.commons.beanutils.BeanUtils.copyProperties(Object target, Object source);
    
    org.springframework.beans.BeanUtils.copyProperties(Object source, Object target);
    
    从方法签名上可以看出,这**两个工具类的名称相同,方法名也相同,甚至连参数个数、类型、名称都相同**。但是**参数的位置是相反的**。因此,如果你想更改的时候,千万要记得,将 target 和 source 两个参数也调换过来!
  4. Apache BeanUtils.copyProperties 有一种线程级别的“缓存”,第一次刷新缓存耗时较长,后续直接读”缓存”耗时较短,这种“缓存”是线程粒度

    • 单线程模型下,第一次访问BeanUtils.copyProperties耗时有200-300ms左右,后续访问几乎都是0ms,也就是微秒级别

    • 并发模型下,每个线程访问BeanUtils.copyProperties会有一次200-300ms耗时, 也就是高性能耗时次数与并发线程数一致

  5. Apache BeanUtils.copyProperties()可以在一定范围内进行类型转换(比如 int —> Integer),同时还要注意一些不能转换时候,会将默认null值转化成0;

  6. Apache BeanUtils 和 Apache PropertyUtils两个工具类都是对bean之间存在属性名相同的属性进行处理,无论是源bean或者是目标bean中多出来的属性均不处理

  7. Apache BeanUtils.copyProperties()可以在一定范围内进行类型转换(比如 int —> Integer),同时还要注意一些不能转换时候,会将默认null值转化成0;

  8. Property.copyProperties()则是严格的类型转化,必须类型和属性名完全一致才转化。PropertyUtils 支持为null的场景

对 Spring BeanUtils 的总结

  1. Spring BeanUtils 和 Apache BeanUtils 同名。从方法签名上可以看出,这两个工具类的名称相同,方法名也相同,甚至连参数个数、类型、名称都相同。但是参数的位置是相反的。因此,如果你想更改的时候,千万要记得,将 target 和 source 两个参数也调换过来!

  2. Spring BeanUtils.copryProperties()也可以在一定范围内进行类型转换(比如 int —> Integer)BeanUtils 对部分属性不支持 null,具体如下

    • java.util.Date 类型不支持,但是它的自雷java.sql.Date是被支持的。java.util.Date直接copy会报异常

    • Boolean,Integer,Long等不支持,会将null转化为0

    • String支持,转化后依然为null

  3. 如果存在属性完全相同的内部类,但是不是同一个内部类,即分别属于各自的内部类,则spring会认为属性不同,不会copy;

对 Cglib BeanCopier 的总结

  1. BeanCopier支持两种方式:

    • 一种是不使用Converter的方式,仅对两个bean间属性名和类型完全相同的变量进行拷贝;

    • 另一种则引入Converter,可以对某些特定属性值进行特殊操作

  2. Cglib 使用 BeanCopier.create()也是非常耗时,避免多次调用,尽可能做成全局初始化一次

  3. Cglib BeanCopier 的主要耗时方法就在 BeanCopier.create(),如果将该方法做成静态成员变量,则还可以大大缩小执行时间。BeanCopier是一种基于字节码的方式,其实就是通过字节码方式转换成性能最好的get、set方式,只需考虑创建BeanCopier的开销,如果我们将BeanCopier做成静态的,基本只需考虑get、set的开销,所以性能接近于get、set

  4. Cglib 的 BeanCopier 只拷贝名称和类型都相同的属性

    即使源类型是原始类型(int, shortchar等),目标类型是其包装类型(Integer, ShortCharacter等),或反之:都不会被拷贝

https://www.jianshu.com/p/2ca157963698)

拷贝严格性检验

* Apache BeanUtils.copyProperties()可以在一定范围内进行类型转换(比如 int ---> Integer),同时还要注意一些不能转换时候,会将默认null值转化成0;
* Property.copyProperties()则是严格的类型转化,必须类型和属性名完全一致才转化。PropertyUtils 支持为null的场景
* Spring BeanUtils.copryProperties()也可以在一定范围内进行类型转换(比如 int ---> Integer)BeanUtils 对部分属性不支持 null,具体如下
   a. java.util.Date 类型不支持,但是它的自雷java.sql.Date是被支持的。java.util.Date直接copy会报异常;
   b. Boolean,Integer,Long等不支持,会将null转化为0;
   c. String支持,转化后依然为null

参考链接:这四种对象属性拷贝方式,你都知道吗? – 风尘博客 – 博客园 (cnblogs.com)

BeanUtils.copyProperties(A,B)使用注意事项 – 龙-OSCAR – 博客园 (cnblogs.com)

BeanCopier、BeanUtils对象属性拷贝 – 简书 (jianshu.com)

Spring的BeanUtils的copyProperties方法需要注意的点 – 简书 (jianshu.com)

几种copyProperties工具类性能比较 – 简书 (jianshu.com)

Java Bean Copy框架性能对比-阿里云开发者社区 (aliyun.com)

为什么阿里要求避免使用 Apache BeanUtils 进行属性复制? – 简书 (jianshu.com)