APP下载

干货--你喜欢的Apache BeanUtils 原来有这么大的坑

消息来源:baojiabao.com 作者: 发布时间:2026-05-23

报价宝综合消息干货--你喜欢的Apache BeanUtils 原来有这么大的坑

缘起

有一次开发过程中,刚好看到一个小伙伴在呼叫 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 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次

StaticCglibBeanCopier0.0550220.5410290.9994782.7548249.88556

CglibBeanCopier5.32079811.08632361.03744672.484607333.384007

SpringBeanUtils5.18048321.32854230.021662103.266375966.439272

CommonsPropertyUtils9.72915942.92735674.063789386.1277871955.5437

CommonsBeanUtils24.99513170.728558572.3353272970.306827563.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/bcbacab3b89e

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

Java Bean Copy框架效能对比:https://yq.aliyun.com/articles/392185

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

2019-12-17 13:54:00

相关文章