阿里代码规范要求避免使用Apache BeanUtils进行属性复制
起因
在一次开发过程中,刚好看到小伙伴在调用set方法,将数据库中查询出来的数据PO对象的属性拷贝到VO对象中。
当PO和VO的两个对象的字段属性绝大部分是一样的,我们一个一个的set做了大量的重复工作,而且这种操作很容易出错,因为对象属性太多,有可能漏掉或者重复set肉眼很难发现。
类似这种操作我们很容易想到可以通过反射解决。其实用一个BeanUtils工具类就可以搞定了。
但是如果使用Apache的BeanUtils.copyPropreties进行属性拷贝,这就是一个坑
阿里代码规范
当我们开启阿里的代码扫描插件时,如果使用Apache的BeanUtils.copyPropreties进行属性拷贝,它会给一个非常严重的警告。因为,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|
"
).append(
"|----|----|----|----|----|----|
"
);
@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(
"
"
);
}
@AfterClass
public static void tearDown() throws
Exception
{
System
.out.println(
"测试结果:"
);
System
.out.println(resultBuilder);
}
}
测试结果
实现 | 100 | 1,000 |
---|---|---|
StaticCglibBeanCopier | 0.0563361 | 0.680016 |
CglibBeanCopier | 4.099259 | 12.252336 |
SpringBeanUitils | 3.80229 | 9.268228 |
CommonsPropertyUtils | 6,797116 | 20.59255 |
结果表明,Cglib 的 BeanCopier 的拷贝速度是最快的,即使是百万次的拷贝也只需要 10 毫秒! 相比而言,最差的是 Commons 包的 BeanUtils.copyProperties 方法,100 次拷贝测试与表现最好的 Cglib 相差 400 倍之多
原因分析
查看源码,我们会发现 CommonsBeanUtils 主要有以下几个耗时的地方:
1.输出了大量的日志调试信息
2.重复的对象类型检查
3.类型转换
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