升级到Spring 5.3.14之后,BeanUtils.copyProperties()方法失效,我TM方了!

1,688 阅读4分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路

祸端

最近项目转测当天,突然发现web页面展示是空页面,前台开发人员一顿排查,啥也没发现。也难怪,当前版本web改动还挺大的,大家把关注点都集中在了前台,以为是前台代码哪里出了bug。

然,“祸起萧墙” 却是后端,因升级spring引起,由版本5.2.19.RELEASE->5.3.14版本。

祸起萧墙

故事的开始

项目本来使用spring全家桶是在5.2.19.RELEASE版本,业务运行正常且平稳,一切都向着美好的样子发展。

美好.PNG

夜黑风高的晚上,为了解决某漏洞(不是重点,忽略),升级了spring版本,犹如一个巨大的黑洞把所有美好都吸走进无边无际暗黑。有点跑题哈。。。拉回来,拉回来。

【往事如常】5.19.RELEASE版本

交代下业务背景,是web页面需要展示所有卡片样式信息,展示哪些卡片样式呢?这些都是由后台返回给前端。

大白话就是,后台给数据,前台来渲染。这也是目前通用玩法。

image.png

关键代码,拷贝属性值。

public PortalCardStyleQueryForPageResponse
    createPortalCardStyleQueryForPageResponse(CardStyleQueryForPageResponse cardStyleQueryForPageResponse) {
    PortalCardStyleQueryForPageResponse portalCardStyleQueryForPageResponse =
        new PortalCardStyleQueryForPageResponse();
        // 这就是始作俑者
    BeanUtils.copyProperties(cardStyleQueryForPageResponse, portalCardStyleQueryForPageResponse);
    return portalCardStyleQueryForPageResponse;
}

BeanUtils.copyProperties(cardStyleQueryForPageResponse, portalCardStyleQueryForPageResponse);作用之后,可以拷贝并拿到属性值,可见item字段是有值的。

有值的.PNG

登录机器查看spring全家桶使用版本,也都是5.2.19.RELEASE 5.2.19spring.PNG

故事没有高潮,直接End

一切都变了,我被现实重重抽了巴掌。

image.png

【活在当下】5.3.14版本

业务场景没有变,代码也没有变,但是结果却变了。 BeanUtils.copyProperties(cardStyleQueryForPageResponse, portalCardStyleQueryForPageResponse);作用之后,除了字段item=null外,其他字段都有值。

无值.PNG

TM就是这么方,当时的感觉就是,一直使用的代码怎么就出问题了呢?遵循的价值观顷刻间崩塌,进入吃瓜状态。

image.png

祸从何来

就是从升级spring 5.3.14开始,厄运如影随行,像一颗定时炸弹,备不住什么时候就“哐当”一声。这次就把我给炸懵了。 201108260652502432.gif

好在哥们自愈能力极强,立马复盘。

翻看spring源码,5.3.14版本

private static void copyProperties(Object source, Object target, @Nullable Class<?> editable,
      @Nullable String... ignoreProperties) throws BeansException {

   Assert.notNull(source, "Source must not be null");
   Assert.notNull(target, "Target must not be null");

   Class<?> actualEditable = target.getClass();
   if (editable != null) {
      if (!editable.isInstance(target)) {
         throw new IllegalArgumentException("Target class [" + target.getClass().getName() +
               "] not assignable to Editable class [" + editable.getName() + "]");
      }
      actualEditable = editable;
   }
   // 获取target类的所有属性Descriptor,当然也包括重要的getter/setter
   PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);
   List<String> ignoreList = (ignoreProperties != null ? Arrays.asList(ignoreProperties) : null);

    // 这个遍历很重要,就是完成每个属性值的赋予
   for (PropertyDescriptor targetPd : targetPds) {
       // 获取当前属性的setter
      Method writeMethod = targetPd.getWriteMethod();
      if (writeMethod != null && (ignoreList == null || !ignoreList.contains(targetPd.getName()))) {
          // 获取targetBean属性对应的sourceBean的属性Descriptor
         PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName());
         if (sourcePd != null) {
             // 获取sourceBean属性的getter,这里就是与上面的targetBean的setter对应的。也可以看出如果类没有setter/getter方法,也是无法完成属性拷贝的。
            Method readMethod = sourcePd.getReadMethod();
            if (readMethod != null) {
               ResolvableType sourceResolvableType = ResolvableType.forMethodReturnType(readMethod);
               ResolvableType targetResolvableType = ResolvableType.forMethodParameter(writeMethod, 0);
                // 从源码英文注释可以看出,问题就是出在这里。判断属性类型是否一致,包括检查泛型是不是一致的。
               // Ignore generic types in assignable check if either ResolvableType has unresolvable generics.
               boolean isAssignable =
                     (sourceResolvableType.hasUnresolvableGenerics() || targetResolvableType.hasUnresolvableGenerics() ?
                           ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType()) :
                           targetResolvableType.isAssignableFrom(sourceResolvableType));

               if (isAssignable) {
                  try {
                     if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {
                        readMethod.setAccessible(true);
                     }
                     Object value = readMethod.invoke(source);
                     if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
                        writeMethod.setAccessible(true);
                     }
                     writeMethod.invoke(target, value);
                  }
                  catch (Throwable ex) {
                     throw new FatalBeanException(
                           "Could not copy property '" + targetPd.getName() + "' from source to target", ex);
                  }
               }
            }
         }
      }
   }
}

如上面代码,自添加注释可以看出:

image.png 类属性有泛型且source和target类型不一致时,直接跳过,不再进行属性拷贝。

放到我们的业务上来回放,明显可以看出:

  1. sourceBean是CardStyleQueryForPageResponse类,其中有个属性List<CardStyleAttr> items是list<T>的泛型。

    image.png

  2. targetBean是PortalCardStyleQueryForPageResponse类,其中也有个属性List<PortalCardStyleGetResponse> items也是List<T>的泛型。

    image.png

  3. 且二者的泛型的具体类又不一致,sourceBean使用CardStyleAttr、targetBean使用PortalCardStyleGetResponse;如此,正好与源码所述一致,即被忽略,不再赋值。

而在未升级之前的5.2.19.RELEASE版本,源码中其实并没有校验泛型是否一致这么一步。所以在对于未升级spring之前的业务版本,也一直是岁月静好。

我来算一算,怎么逢祸化吉?

BeanUtils.copyProperties可以用来作为Bean之间拷贝的快捷方法,但是对于一些本身Bean属性就很复杂的,还是不建议使用。

再者说了,源码大家也看到了,用到了反射取targetBean的setter、取sourceBean的getter来完成拷贝动作。这种方式相较于咱们直接手动写get/set完成赋值动作,性能消耗更大。

其实,我本人还是愿意手写get/set的,一者代码可阅读性更好,二者也花费不了太多时间。