为什么阿里代码规约要求避免使用 Apache BeanUtils 进行属性复制

缘起

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

640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1

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

类似这样的操作,我们很容易想到可以通过反射来解决。其实,如此普遍通用的功能,一个 BeanUtils 工具类就可以搞定了。

于是我建议这位小伙伴了解一下 BeanUtils,后来他使用了 Apache BeanUtils.copyProperties 进行属性拷贝,这为程序挖了一个坑

阿里代码规约

当我们开启阿里代码扫描插件时,如果你使用了 Apache BeanUtils.copyProperties 进行属性拷贝,它会给你一个非常严重的警告。因为,Apache BeanUtils性能较差,可以使用 Spring BeanUtils 或者 Cglib BeanCopier 来代替

640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1

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

那么,这个性能问题究竟是有多严重呢?毕竟,在我们的应用场景中,如果只是很微小的性能损耗,但是能带来非常大的便利性,还是可以接受的。

带着这个问题。我们来做一个实验,验证一下。

如果对具体的测试方式没有兴趣,可以跳过直接看结果哦~

测试方法接口和实现定义

首先,为了测试方便,让我们来定义一个接口,并提供各种实现:

 
  
  1. public interface PropertiesCopier {

  2. void copyProperties(Object source, Object target) throws Exception;

  3. }

  4. public class CglibBeanCopierPropertiesCopier implements PropertiesCopier {

  5. @Override

  6. public void copyProperties(Object source, Object target) throws Exception {

  7. BeanCopier copier = BeanCopier.create(source.getClass(), target.getClass(), false);

  8. copier.copy(source, target, null);

  9. }

  10. }

  11. // 全局静态 BeanCopier,避免每次都生成新的对象

  12. public class StaticCglibBeanCopierPropertiesCopier implements PropertiesCopier {

  13. private static BeanCopier copier = BeanCopier.create(Account.class, Account.class, false);

  14. @Override

  15. public void copyProperties(Object source, Object target) throws Exception {

  16. copier.copy(source, target, null);

  17. }

  18. }

  19. public class SpringBeanUtilsPropertiesCopier implements PropertiesCopier {

  20. @Override

  21. public void copyProperties(Object source, Object target) throws Exception {

  22. org.springframework.beans.BeanUtils.copyProperties(source, target);

  23. }

  24. }

  25. public class CommonsBeanUtilsPropertiesCopier implements PropertiesCopier {

  26. @Override

  27. public void copyProperties(Object source, Object target) throws Exception {

  28. org.apache.commons.beanutils.BeanUtils.copyProperties(target, source);

  29. }

  30. }

  31. public class CommonsPropertyUtilsPropertiesCopier implements PropertiesCopier {

  32. @Override

  33. public void copyProperties(Object source, Object target) throws Exception {

  34. org.apache.commons.beanutils.PropertyUtils.copyProperties(target, source);

  35. }

  36. }

单元测试

然后写一个参数化的单元测试:

 
  
  1. @RunWith(Parameterized.class)

  2. public class PropertiesCopierTest {

  3. @Parameterized.Parameter(0)

  4. public PropertiesCopier propertiesCopier;

  5. // 测试次数

  6. private static List<Integer> testTimes = Arrays.asList(100, 1000, 10_000, 100_000, 1_000_000);

  7. // 测试结果以 markdown 表格的形式输出

  8. private static StringBuilder resultBuilder = new StringBuilder("|实现|100|1,000|10,000|100,000|1,000,000|\n").append("|----|----|----|----|----|----|\n");


  9. @Parameterized.Parameters

  10. public static Collection<Object[]> data() {

  11. Collection<Object[]> params = new ArrayList<>();

  12. params.add(new Object[]{new StaticCglibBeanCopierPropertiesCopier()});

  13. params.add(new Object[]{new CglibBeanCopierPropertiesCopier()});

  14. params.add(new Object[]{new SpringBeanUtilsPropertiesCopier()});

  15. params.add(new Object[]{new CommonsPropertyUtilsPropertiesCopier()});

  16. params.add(new Object[]{new CommonsBeanUtilsPropertiesCopier()});

  17. return params;

  18. }


  19. @Before

  20. public void setUp() throws Exception {

  21. String name = propertiesCopier.getClass().getSimpleName().replace("PropertiesCopier", "");

  22. resultBuilder.append("|").append(name).append("|");

  23. }


  24. @Test

  25. public void copyProperties() throws Exception {

  26. Account source = new Account(1, "test1", 30D);

  27. Account target = new Account();

  28. // 预热一次

  29. propertiesCopier.copyProperties(source, target);

  30. for (Integer time : testTimes) {

  31. long start = System.nanoTime();

  32. for (int i = 0; i < time; i++) {

  33. propertiesCopier.copyProperties(source, target);

  34. }

  35. resultBuilder.append((System.nanoTime() - start) / 1_000_000D).append("|");

  36. }

  37. resultBuilder.append("\n");

  38. }


  39. @AfterClass

  40. public static void tearDown() throws Exception {

  41. System.out.println("测试结果:");

  42. System.out.println(resultBuilder);

  43. }

  44. }

测试结果

640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1

结果表明,Cglib 的 BeanCopier 的拷贝速度是最快的,即使是百万次的拷贝也只需要 10 毫秒! 相比而言,最差的是 Commons 包的 BeanUtils.copyProperties 方法,100 次拷贝测试与表现最好的 Cglib 相差 400 倍之多。百万次拷贝更是出现了 2600 倍的性能差异!

结果真是让人大跌眼镜。

但是它们为什么会有这么大的差异呢?

原因分析

查看源码,我们会发现 CommonsBeanUtils 主要有以下几个耗时的地方:

  • 输出了大量的日志调试信息

  • 重复的对象类型检查

  • 类型转换

 
  
  1. public void copyProperties(final Object dest, final Object orig)

  2. throws IllegalAccessException, InvocationTargetException {

  3. // 类型检查

  4. if (orig instanceof DynaBean) {

  5. ...

  6. } else if (orig instanceof Map) {

  7. ...

  8. } else {

  9. final PropertyDescriptor[] origDescriptors = ...

  10. for (PropertyDescriptor origDescriptor : origDescriptors) {

  11. ...

  12. // 这里每个属性都调一次 copyProperty

  13. copyProperty(dest, name, value);

  14. }

  15. }

  16. }


  17. public void copyProperty(final Object bean, String name, Object value)

  18. throws IllegalAccessException, InvocationTargetException {

  19. ...

  20. // 这里又进行一次类型检查

  21. if (target instanceof DynaBean) {

  22. ...

  23. }

  24. ...

  25. // 需要将属性转换为目标类型

  26. value = convertForCopy(value, type);

  27. ...

  28. }

  29. // 而这个 convert 方法在日志级别为 debug 的时候有很多的字符串拼接

  30. public <T> T convert(final Class<T> type, Object value) {

  31. if (log().isDebugEnabled()) {

  32. log().debug("Converting" + (value == null ? "" : " '" + toString(sourceType) + "'") + " value '" + value + "' to type '" + toString(targetType) + "'");

  33. }

  34. ...

  35. if (targetType.equals(String.class)) {

  36. return targetType.cast(convertToString(value));

  37. } else if (targetType.equals(sourceType)) {

  38. if (log().isDebugEnabled()) {

  39. log().debug("No conversion required, value is already a " + toString(targetType));

  40. }

  41. return targetType.cast(value);

  42. } else {

  43. // 这个 convertToType 方法里也需要做类型检查

  44. final Object result = convertToType(targetType, value);

  45. if (log().isDebugEnabled()) {

  46. log().debug("Converted to " + toString(targetType) + " value '" + result + "'");

  47. }

  48. return targetType.cast(result);

  49. }

  50. }

具体的性能和源码分析,可以参考这几篇文章:

几种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 为了解决Date为空的问题会导致为目标对象的原始类型的包装类属性赋予初始值,如 Integer 属性默认赋值为 0,尽管你的来源对象该字段的值为 null。

这个在我们的包装类属性为 null 值时有特殊含义的场景,非常容易踩坑!例如搜索条件对象,一般 null 值表示该字段不做限制,而 0 表示该字段的值必须为0。

改用其他工具时

当我们看到阿里的提示,或者你看了这篇文章之后,知道了 CommonsBeanUtils 的性能问题,想要改用 Spring 的 BeanUtils 时,要特别小心

 
  
  1. org.apache.commons.beanutils.BeanUtils.copyProperties(Object target, Object source);

  2. org.springframework.beans.BeanUtils.copyProperties(Object source, Object target);

从方法签名上可以看出,这两个工具类的名称相同,方法名也相同,甚至连参数个数、类型、名称都相同。但是参数的位置是相反的。因此,如果你想更改的时候,千万要记得,将 target 和 source 两个参数也调换过来!

原文地址:https://mp.weixin.qq.com/s?__biz=MzA3ODQ0Mzg2OA==&mid=2649049711&idx=1&sn=6d874638eea770918a599be2442f77de&chksm=87534e5cb024c74a3c2a0dd792ac1e85ef4c7260235ff029a812300261d230410e418a58f6c0&mpshare=1&scene=23&srcid=06115dzep7FXbo031oAmGRHn#rd

转载于:https://www.cnblogs.com/jpfss/p/11001788.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值