前言
前段时间在调用rpc的时候,发现rpc返回结果中存在一个没有被定义的类,简单来说就是响应体Class并没有定义这个类,但它却结结实实的出现在响应中,这就离了个大谱!
于是我直接clone下目标project,直接翻rpc一探究竟!
场景复现
这里就不扯东扯西了,看文章标题就知道,最后是BeanUtils搞的鬼,下面我直接用一段代码复现出问题
java复制代码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<InnerUser> list;
}
@Data
class Person {
private Integer age;
private List<InnerPerson> list;
}
@Data
class InnerUser {
private String inner;
}
@Data
class InnerPerson {
private String inner;
}
假设Person类就是我的rpc返回结果的话,此时Person类中并不存在InnerUser类,但它却出现了响应结果中,这就离谱,我根本不认识它!
什么原因?
有细心小伙伴会发现我在User和Person类中,都定义了list字段,但是User类中是List<InnerUser>,而Person类中是List<InnerPerson> list,两者泛型不同,应该是不能copy成功的,但是现在却成功了,而且还把InnerUser也copy过来了!
下面来看BeanUtils.copyProperties源码中对于source和target字段的比较逻辑
java复制代码private static void copyProperties(Object source, Object target, @Nullable Class<?> editable,
@Nullable String... ignoreProperties) throws BeansException {
PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);
List<String> 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已经解决了这个问题
java复制代码private static void copyProperties(Object source, Object target, @Nullable Class<?> editable,
@Nullable String... ignoreProperties) throws BeansException {
PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);
List<String> 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版本不是很新,才会让我在开发过程中遇到这个问题,故梳理出本文稍微总结一下~