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

栏目: 编程工具 · 发布时间: 4年前

内容简介:声明:本文属原创文章,始发于公号:有一次开发过程中,刚好看到一个小伙伴在调用 set 方法,将一个数据库中查询出来的 PO 对象的属性拷贝到 Vo 对象中,类似这样:

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

缘起

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

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

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

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

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

阿里代码规约

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

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

看到这样的警告,有点让人有点不爽。大名鼎鼎的 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<Integer> 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|\n").append("|----|----|----|----|----|----|\n");

    @Parameterized.Parameters
    public static Collection<Object[]> data() {
        Collection<Object[]> 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("\n");
    }

    @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> T convert(final Class<T> 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);
        }
    }

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

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 值导致的。


以上所述就是小编给大家介绍的《为什么阿里代码规约要求避免使用 Apache BeanUtils 进行属性的拷贝》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Large-Scale Inference

Large-Scale Inference

Bradley Efron / Cambridge University Press / 2010-8-5 / GBP 48.00

We live in a new age for statistical inference, where modern scientific technology such as microarrays and fMRI machines routinely produce thousands and sometimes millions of parallel data sets, each ......一起来看看 《Large-Scale Inference》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码