有一次开发过程中,刚好看到小伙伴在调用 set 方法,将数据库中查询出来的 Po 对象的属性拷贝到 Vo 对象中,类似这样:
可以看出,Po 和 Vo 两个类的字段绝大部分是一样的,我们一个个地调用 set 方法只是做了一些重复的冗长的操作。这种操作非常容易出错,因为对象的属性太多,有可能会漏掉一两个,而且肉眼很难察觉。
类似这样的操作,我们很容易想到可以通过反射来解决。其实,如此普遍通用的功能,一个 BeanUtils 工具类就可以搞定了。
于是我建议这位小伙伴了解一下 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 倍之多。百万次拷贝更是出现了 2600 倍的性能差异!
结果真是让人大跌眼镜。
但是它们为什么会有这么大的差异呢?
原因分析
查看源码,我们会发现 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);
}
}
具体的性能和源码分析,可以参考这几篇文章:
几种copyProperties工具类性能比较:https://www.jianshu.com/p/bcbacab3b89e
CGLIB中BeanCopier源码实现:https://www.jianshu.com/p/f8b892e08d26
Java Bean Copy框架性能对比:https://yq.aliyun.com/articles/392185
除了性能问题之外,在使用 CommonsBeanUtils 时还有其他的坑需要特别小心!
包装类默认值
在进行属性拷贝时,低版本CommonsBeanUtils 为了解决Date为空的问题会导致为目标对象的原始类型的包装类属性赋予初始值,如 Integer 属性默认赋值为 0,尽管你的来源对象该字段的值为 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 两个参数也调换过来!
留言与评论(共有 0 条评论) |