阿里规约要求避免使用 Apache BeanUtils 进行属性的拷贝的深层意义?

在这里插入图片描述
最近对于项目代码扫描的时候,发现了一个BeanUtils.copyProperties的小问题,当时的备注是:暂时先不处理,
在这里插入图片描述
在这里插入图片描述
根据这个线索:开始寻找我的意中人;众里寻她千百度,蓦然回首那人却在灯火阑珊处,盘着腿,抽着烟,好似一个活神仙,我与他的故事才刚刚开始:接下来是他的经历
在这里插入图片描述
有一次开发过程中,刚好看到一个小伙伴在调用 set 方法将一个数据库中查询出来的 PO 对象的 20 多个属性拷贝到 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<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);
    }
}

测试结果
时间单位毫秒
实现
在这里插入图片描述
结果表明,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);
        }
    }

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

扫描二维码关注公众号,回复: 12306995 查看本文章
  • 几种copyProperties工具类性能比较:www.jianshu.com/p/bcbacab3b…
  • CGLIB中BeanCopier源码实现:www.jianshu.com/p/f8b892e08…
  • Java Bean Copy框架性能对比:yq.aliyun.com/articles/39…
    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://juejin.im/post/5d0b68a36fb9a07ee1692ed9

猜你喜欢

转载自blog.csdn.net/chajinglong/article/details/104944942