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

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

缘起

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

属性拷贝

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

相似这样的操做,咱们能够很容易想到,能够经过反射来解决。其实,如此广泛通用的功能,一个 BeanUtils 工具类就能够搞定了。数据库

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

阿里代码规约

当咱们开启阿里代码扫描插件时,若是你使用了 Apache BeanUtils.copyProperties 进行属性拷贝,它会给你一个很是严重的警告。由于,Apache BeanUtils性能较差,可使用 Spring BeanUtils 或者 Cglib BeanCopier 来代替markdown

阿里规约警告

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

那么,这个性能问题到底是有多严重呢?毕竟,在咱们的应用场景中,若是只是很微小的性能损耗,可是能带来很是大的便利性,仍是能够接受的。框架

带着这个问题。咱们来作一个实验,验证一下。ide

若是对具体的测试方式没有兴趣,能够跳过直接看结果哦~工具

测试方法接口和实现定义

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

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

相关文章
相关标签/搜索