前言
前段时间在调用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类,但它却出现了响应结果中,这就离谱,我根本不认识它!
什么原因?
有细心小伙伴会发现我在User和Person类中,都定义了list字段,但是User类中是List
下面来看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成功。
解决办法
升级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); } } } } } } }
通过源码可见
如果source具备不可解析的泛型,则直接为true
source为可解析泛型
target为不可解析泛型,则忽略泛型,直接比较字段原始类型
target为可解析泛型,则比较source、target泛型
扩展点
其实如果只是要解决本文所提出的泛型问题,最低升级spring-beans版本到5.3.0就可解决问题,但上面为什么说要升级到5.3.27呢?
因为上面提到了一个概念: 不可解析的泛型
在5.3.0中已经修改为通过泛型比较了,但是由于泛型擦除机制存在,泛型擦除后,一切归为原始类型,即不可解析泛型,直接通过isAssignableFrom判断可能会不太精确
所以如上面小结所见, 在5.3.27版本中,当source、target都为不可解析泛型时,还是选择用字段原始类型进行比较
总结
有关BeanUtils的泛型问题虽然大部分同学都知道一些,但是可能实际业务开发过程中并不会遇到,本文也是由于项目所使用的spring版本不是很新,才会让我在开发过程中遇到这个问题,故梳理出本文稍微总结一下~
发表评论 取消回复