资讯专栏INFORMATION COLUMN

为什么阿里代码规约要求避免使用 Apache BeanUtils 进行属性的拷贝

raledong / 887人阅读

摘要:于是我建议这位小伙伴使用了进行属性拷贝,这为我们的程序挖了一个坑阿里代码规约当我们开启阿里代码扫描插件时,如果你使用了进行属性拷贝,它会给你一个非常严重的警告。大名鼎鼎的提供的包,居然会存在性能问题,以致于阿里给出了严重的警告。

声明:本文属原创文章,始发于公号:程序员自学之道,并同步发布于 https://blog.csdn.net/dadiyang,特此,同步发布到 sf,转载请注明出处。

缘起

有一次开发过程中,刚好看到一个小伙伴在调用 set 方法,将一个数据库中查询出来的 PO 对象的属性拷贝到 Vo 对象中,类似这样:

可以看出,Po 和 Vo 两个类的字段绝大部分是一样的,我们一个个地调用 set 方法只是做了一些重复的冗长的操作。这种操作非常容易出错,因为对象的属性太多,有可能会漏掉一两个,而且肉眼很难察觉

类似这样的操作,我们可以很容易想到,可以通过反射来解决。其实,如此普遍通用的功能,一个 BeanUtils 工具类就可以搞定了。

于是我建议这位小伙伴使用了 Apache BeanUtils.copyProperties 进行属性拷贝,这为我们的程序挖了一个坑

阿里代码规约

当我们开启阿里代码扫描插件时,如果你使用了 Apache BeanUtils.copyProperties 进行属性拷贝,它会给你一个非常严重的警告。因为,Apache BeanUtils性能较差,可以使用 Spring BeanUtils 或者 Cglib BeanCopier 来代替

看到这样的警告,有点让人有点不爽。大名鼎鼎的 Apache 提供的包,居然会存在性能问题,以致于阿里给出了严重的警告。

那么,这个性能问题究竟是有多严重呢?毕竟,在我们的应用场景中,如果只是很微小的性能损耗,但是能带来非常大的便利性,还是可以接受的。

带着这个问题。我们来做一个实验,验证一下。

如果对具体的测试方式没有兴趣,可以跳过直接看结果哦~

测试方法接口和实现定义

首先,为了测试方便,让我们来定义一个接口,并将几种实现统一起来:

public interface PropertiesCopier {
    void copyProperties(Object source, Object target) throws Exception;
}
public class CglibBeanCopierPropertiesCopier implements PropertiesCopier {
    @Override
    public void copyProperties(Object source, Object target) throws Exception {
        BeanCopier copier = BeanCopier.create(source.getClass(), target.getClass(), false);
        copier.copy(source, target, null);
    }
}
// 全局静态 BeanCopier,避免每次都生成新的对象
public class StaticCglibBeanCopierPropertiesCopier implements PropertiesCopier {
    private static BeanCopier copier = BeanCopier.create(Account.class, Account.class, false);
    @Override
    public void copyProperties(Object source, Object target) throws Exception {
        copier.copy(source, target, null);
    }
}
public class SpringBeanUtilsPropertiesCopier implements PropertiesCopier {
    @Override
    public void copyProperties(Object source, Object target) throws Exception {
        org.springframework.beans.BeanUtils.copyProperties(source, target);
    }
}
public class CommonsBeanUtilsPropertiesCopier implements PropertiesCopier {
    @Override
    public void copyProperties(Object source, Object target) throws Exception {
        org.apache.commons.beanutils.BeanUtils.copyProperties(target, source);
    }
}
public class CommonsPropertyUtilsPropertiesCopier implements PropertiesCopier {
    @Override
    public void copyProperties(Object source, Object target) throws Exception {
        org.apache.commons.beanutils.PropertyUtils.copyProperties(target, source);
    }
}
单元测试

然后写一个参数化的单元测试:

@RunWith(Parameterized.class)
public class PropertiesCopierTest {
    @Parameterized.Parameter(0)
    public PropertiesCopier propertiesCopier;
    // 测试次数
    private static List testTimes = Arrays.asList(100, 1000, 10_000, 100_000, 1_000_000);
    // 测试结果以 markdown 表格的形式输出
    private static StringBuilder resultBuilder = new StringBuilder("|实现|100|1,000|10,000|100,000|1,000,000|
").append("|----|----|----|----|----|----|
");

    @Parameterized.Parameters
    public static Collection data() {
        Collection params = new ArrayList<>();
        params.add(new Object[]{new StaticCglibBeanCopierPropertiesCopier()});
        params.add(new Object[]{new CglibBeanCopierPropertiesCopier()});
        params.add(new Object[]{new SpringBeanUtilsPropertiesCopier()});
        params.add(new Object[]{new CommonsPropertyUtilsPropertiesCopier()});
        params.add(new Object[]{new CommonsBeanUtilsPropertiesCopier()});
        return params;
    }

    @Before
    public void setUp() throws Exception {
        String name = propertiesCopier.getClass().getSimpleName().replace("PropertiesCopier", "");
        resultBuilder.append("|").append(name).append("|");
    }

    @Test
    public void copyProperties() throws Exception {
        Account source = new Account(1, "test1", 30D);
        Account target = new Account();
        // 预热一次
        propertiesCopier.copyProperties(source, target);
        for (Integer time : testTimes) {
            long start = System.nanoTime();
            for (int i = 0; i < time; i++) {
                propertiesCopier.copyProperties(source, target);
            }
            resultBuilder.append((System.nanoTime() - start) / 1_000_000D).append("|");
        }
        resultBuilder.append("
");
    }

    @AfterClass
    public static void tearDown() throws Exception {
        System.out.println("测试结果:");
        System.out.println(resultBuilder);
    }
}
测试结果

时间单位毫秒

实现 100次 1,000次 10,000次 100,000次 1,000,000次
StaticCglibBeanCopier 0.055022 0.541029 0.999478 2.754824 9.88556
CglibBeanCopier 5.320798 11.086323 61.037446 72.484607 333.384007
SpringBeanUtils 5.180483 21.328542 30.021662 103.266375 966.439272
CommonsPropertyUtils 9.729159 42.927356 74.063789 386.127787 1955.5437
CommonsBeanUtils 24.99513 170.728558 572.335327 2970.3068 27563.3459

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

结果真是让人大跌眼镜。

但是它们为什么会有这么大的差异呢?

原因分析

查看源码,我们会发现 CommonsBeanUtils 主要有以下几个耗时的地方:

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

重复的对象类型检查

类型转换

  public void copyProperties(final Object dest, final Object orig)
        throws IllegalAccessException, InvocationTargetException {
        // 类型检查 
        if (orig instanceof DynaBean) {
            ...
        } else if (orig instanceof Map) {
           ...
        } else {
            final PropertyDescriptor[] origDescriptors = ...
            for (PropertyDescriptor origDescriptor : origDescriptors) {
                ...
                // 这里每个属性都调一次 copyProperty
                copyProperty(dest, name, value);
            }
        }
    }

    public void copyProperty(final Object bean, String name, Object value)
        throws IllegalAccessException, InvocationTargetException {
        ...
        // 这里又进行一次类型检查
        if (target instanceof DynaBean) {
            ...
        }
        ...
        // 需要将属性转换为目标类型
        value = convertForCopy(value, type);
        ...
    }
    // 而这个 convert 方法在日志级别为 debug 的时候有很多的字符串拼接
    public  T convert(final Class type, Object value) {
        if (log().isDebugEnabled()) {
            log().debug("Converting" + (value == null ? "" : " "" + toString(sourceType) + """) + " value "" + value + "" to type "" + toString(targetType) + """);
        }
        ...
        if (targetType.equals(String.class)) {
            return targetType.cast(convertToString(value));
        } else if (targetType.equals(sourceType)) {
            if (log().isDebugEnabled()) {
                log().debug("No conversion required, value is already a " + toString(targetType));
            }
            return targetType.cast(value);
        } else {
            // 这个 convertToType 方法里也需要做类型检查
            final Object result = convertToType(targetType, value);
            if (log().isDebugEnabled()) {
                log().debug("Converted to " + toString(targetType) + " value "" + result + """);
            }
            return targetType.cast(result);
        }
    }

具体的性能和源码分析,可以参考这几篇文章:

几种copyProperties工具类性能比较:https://www.jianshu.com/p/bcb...

CGLIB中BeanCopier源码实现:https://www.jianshu.com/p/f8b...

Java Bean Copy框架性能对比:https://yq.aliyun.com/article...

One more thing

除了性能问题之外,在使用 CommonsBeanUtils 时还有其他的坑需要特别小心!

包装类默认值

在进行属性拷贝时,虽然 CommonsBeanUtils 默认不会给原始包装类赋默认值的,但是在使用低版本(1.8.0及以下)的时候,如果你的类有 Date 类型属性,而且来源对象中该属性值为 null 的话,就会发生异常:

org.apache.commons.beanutils.ConversionException: No value specified for "Date"

解决这个问题的办法是注册一个 DateConverter:

ConvertUtils.register(new DateConverter(null), java.util.Date.class);

然而这个语句,会导致包装类型会被赋予原始类型的默认值,如 Integer 属性默认赋值为 0,尽管你的来源对象该字段的值为 null。

在高版本(1.9.3)中,日期 null 值的问题和包装类赋默认值的问题都被修复了。

这个在我们的包装类属性为 null 值时有特殊含义的场景,非常容易踩坑!例如搜索条件对象,一般 null 值表示该字段不做限制,而 0 表示该字段的值必须为0。

改用其他工具时

当我们看到阿里的提示,或者你看了这篇文章之后,知道了 CommonsBeanUtils 的性能问题,想要改用 Spring 的 BeanUtils 时,要小心:

org.apache.commons.beanutils.BeanUtils.copyProperties(Object target, Object source);
org.springframework.beans.BeanUtils.copyProperties(Object source, Object target);

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

另外,可能由于种种原因,你获取的堆栈信息不完整找不到问题在哪,所以这里顺便提醒一下:

如果你遇到 java.lang.IllegalArgumentException: Source must not be null 或者 java.lang.IllegalArgumentException: Target must not be null 这样的异常信息却到处找不到原因时,不用找了,这是由于你在 copyProperties 的时候传了 null 值导致的。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/74742.html

相关文章

  • JAVA代码规范

    摘要:前言作为一名全干打字员,干活时经常会被要求使用各种各样的语言去实现各种各样的需求,来回切换起来写的代码就会或多或少有点不规范。今天我们以为例,讲讲在代码中,我们需要注意的某些规范。 前言 作为一名全干打字员,干活时经常会被要求使用各种各样的语言去实现各种各样的需求,来回切换起来写的代码就会或多或少有点不规范。今天我们以JAVA为例,讲讲在代码中,我们需要注意的某些规范。(本文标准依赖于...

    GHOST_349178 评论0 收藏0
  • BeanUtils工具使用细节

    摘要:拷贝操作又一个非常好用的工具类和中分别存在一个,提供了对。除了支持基本类型以及基本类型的数组之外,还支持这些类的对象,其余一概不支持。而且,由于这些类都是采用反射机制实现的,对程序的效率也会有影响。因此,慎用或者使用看效果如何 java bean拷贝操作又一个非常好用的工具类 BeanUitls :spring (org.springframework.beans.BeanUtils)...

    afishhhhh 评论0 收藏0
  • 常识之外规范——阿里java开发手册笔记(全章节)

    摘要:说明这篇文章是我第一次认真阅读阿里巴巴开发手册终极版的笔记。说明本手册明确防止是调用者的责任。一年半载后,那么单元测试几乎处于废弃状态。好的单元测试能够最大限度地规避线上故障。 说明 这篇文章是我第一次(认真)阅读《阿里巴巴 Java 开发手册(终极版)》的笔记。手册本身对规范的讲解已经非常详细了,如果你已经有一定的开发经验并且有良好的编码习惯和意识,会发现大部分规范是符合常识的。所以...

    Martin91 评论0 收藏0
  • 新增16条设计规约阿里巴巴Java开发手册(详尽版)开放下载!

    摘要:熟悉和遵守阿里巴巴开发手册的编程风格,那只是标,而代码可读性的本可以追溯到软件设计阶段。何为条设计规约是根据阿里巴巴实际项目架构经验提炼而成,共条。本次新增的不单是条新的设计规约,还是千万阿里人的技术之心。 摘要:2018年6月,《阿里巴巴Java开发手册》再次刷新代码规范认知,我们新增了16条设计规约!现免费开放下载,不可错过!《阿里巴巴Java开发手册》是阿里内部Java工程师所遵...

    _ang 评论0 收藏0
  • 排名前16Java工具类

    摘要:在中,工具类定义了一组公共方法,这篇文章将介绍中使用最频繁及最通用的工具类。另外,工具类,根据阿里开发手册,包名如果要使用不能带,工具类命名为 在Java中,工具类定义了一组公共方法,这篇文章将介绍Java中使用最频繁及最通用的Java工具类。以下工具类、方法按使用流行度排名,参考数据来源于Github上随机选取的5万个开源项目源码。 一. org.apache.commons.io....

    android_c 评论0 收藏0

发表评论

0条评论

raledong

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<