本网站(662p.com)打包出售,且带程序代码数据,662p.com域名,程序内核采用TP框架开发,需要联系扣扣:2360248666 /wx:lianweikj
精品域名一口价出售:1y1m.com(350元) ,6b7b.com(400元) , 5k5j.com(380元) , yayj.com(1800元), jiongzhun.com(1000元) , niuzen.com(2800元) , zennei.com(5000元)
需要联系扣扣:2360248666 /wx:lianweikj
BeanCopy怎么copy出来个不认识的类!
吴振华 · 152浏览 · 发布于2023-06-29 +关注

前言

前段时间在调用rpc的时候,发现rpc返回结果中存在一个没有被定义的类,简单来说就是响应体Class并没有定义这个类,但它却结结实实的出现在响应中,这就离了个大谱!

于是我直接clone下目标project,直接翻rpc一探究竟!


场景复现

这里就不扯东扯西了,看文章标题就知道,最后是BeanUtils搞的鬼,下面我直接用一段代码复现出问题

public class BeanUtilsTest {

  public static void main(String[] args) {

    User user = new User();
    user.setAge(1);
    InnerUser innerUser = new InnerUser();
    innerUser.setInner("我是属于User的");
    user.setList(Lists.newArrayList(innerUser));

    Person person = new Person();

    // 属性拷贝
    BeanUtils.copyProperties(user, person);

    System.out.println(person);
  }

}

@Data
class User {

  private Integer age;

  private List list;

}

@Data
class Person {

  private Integer age;

  private List list;

}


@Data
class InnerUser {

  private String inner;

}

@Data
class InnerPerson {

  private String inner;

}

假设Person类就是我的rpc返回结果的话,此时Person类中并不存在InnerUser类,但它却出现了响应结果中,这就离谱,我根本不认识它!

image-20230628094645352


什么原因?

有细心小伙伴会发现我在User和Person类中,都定义了list字段,但是User类中是List,而Person类中是Listlist,两者泛型不同,应该是不能copy成功的,但是现在却成功了,而且还把InnerUser也copy过来了!

下面来看BeanUtils.copyProperties源码中对于source和target字段的比较逻辑

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

  PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);
  List ignoreList = (ignoreProperties != null ? Arrays.asList(ignoreProperties) : 
  null);

  for (PropertyDescriptor targetPd : targetPds) {
    // 获取target类当前字段的写入方法,也就是set方法
    Method writeMethod = targetPd.getWriteMethod();
    if (writeMethod != null && (ignoreList == null || !ignoreList.contains
    (targetPd.getName()))) {
      
      // 获取source类中该字段的get方法
      PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), 
      targetPd.getName());
      if (sourcePd != null) {
        Method readMethod = sourcePd.getReadMethod();
        
        // 如果能获取到,且get方法返回类型与set方法的第一个入参类型相匹配,
        则可以invoke填充
        if (readMethod != null &&
            ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], 
            readMethod.getReturnType())) {
          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);
          }
        }
      }
    }
  }
}

通过上面源码结合下面图示,我们可以清晰的看到,copyProperties只比较了字段的类型,如果说字段存在泛型,则并没有去比较,所以上面的案例代码能够copy成功。

image-20230628100420945


解决办法

升级spring-beans版本至5.3.27,在新版本中的BeanUtils已经解决了这个问题

private static void copyProperties(Object source, Object target, @Nullable Class
 editable,
         @Nullable String... ignoreProperties) throws BeansException 
                                   {
  
  PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);
  List ignoreList = (ignoreProperties != null ? Arrays.asList(ignoreProperties) : 
  null);

  for (PropertyDescriptor targetPd : targetPds) {
    Method writeMethod = targetPd.getWriteMethod();
    if (writeMethod != null && (ignoreList == null || !ignoreList.contains
    (targetPd.getName())))
     {
      PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), 
      targetPd.getName());
      if (sourcePd != null) {
        Method readMethod = sourcePd.getReadMethod();
        if (readMethod != null) {
          
          // 拿到字段的泛型类型
          ResolvableType sourceResolvableType = ResolvableType.forMethodReturnType
          (readMethod);
          ResolvableType targetResolvableType = ResolvableType.forMethodParameter
          (writeMethod, 0);

          // 看下面总结
          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);
            }
          }
        }
      }
    }
  }
}

通过源码可见

  1. 如果source具备不可解析的泛型,则直接为true

  2. source为可解析泛型

    1. target为不可解析泛型,则忽略泛型,直接比较字段原始类型

    2. target为可解析泛型,则比较source、target泛型


扩展点

其实如果只是要解决本文所提出的泛型问题,最低升级spring-beans版本到5.3.0就可解决问题,但上面为什么说要升级到5.3.27呢?

因为上面提到了一个概念: 不可解析的泛型

在5.3.0中已经修改为通过泛型比较了,但是由于泛型擦除机制存在,泛型擦除后,一切归为原始类型,即不可解析泛型,直接通过isAssignableFrom判断可能会不太精确

image-20230628211917061

所以如上面小结所见, 在5.3.27版本中,当source、target都为不可解析泛型时,还是选择用字段原始类型进行比较


总结

有关BeanUtils的泛型问题虽然大部分同学都知道一些,但是可能实际业务开发过程中并不会遇到,本文也是由于项目所使用的spring版本不是很新,才会让我在开发过程中遇到这个问题,故梳理出本文稍微总结一下~




相关推荐

PHP实现部分字符隐藏

沙雕mars · 1325浏览 · 2019-04-28 09:47:56
Java中ArrayList和LinkedList区别

kenrry1992 · 908浏览 · 2019-05-08 21:14:54
Tomcat 下载及安装配置

manongba · 970浏览 · 2019-05-13 21:03:56
JAVA变量介绍

manongba · 963浏览 · 2019-05-13 21:05:52
什么是SpringBoot

iamitnan · 1086浏览 · 2019-05-14 22:20:36
加载中

0评论

评论
坐标是江苏.南京,行业是互联网,技术是PHP和java,还有熟悉前后端等。
分类专栏
小鸟云服务器
扫码进入手机网页