声明:本文属原创文章,始发于公号:程序员自学之道,并同步发布于 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); } }
具体的性能和源码分析,能够参考这几篇文章:
除了性能问题以外,在使用 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 值致使的。