本文已参与「新人创作礼」活动,一起开启掘金创作之路
祸端
最近项目转测当天,突然发现web页面展示是空页面,前台开发人员一顿排查,啥也没发现。也难怪,当前版本web改动还挺大的,大家把关注点都集中在了前台,以为是前台代码哪里出了bug。
然,“祸起萧墙” 却是后端,因升级spring引起,由版本5.2.19.RELEASE->5.3.14版本。
祸起萧墙
故事的开始
项目本来使用spring全家桶是在5.2.19.RELEASE版本,业务运行正常且平稳,一切都向着美好的样子发展。
夜黑风高的晚上,为了解决某漏洞(不是重点,忽略),升级了spring版本,犹如一个巨大的黑洞把所有美好都吸走进无边无际暗黑。有点跑题哈。。。拉回来,拉回来。
【往事如常】5.19.RELEASE版本
交代下业务背景,是web页面需要展示所有卡片样式信息,展示哪些卡片样式呢?这些都是由后台返回给前端。
大白话就是,后台给数据,前台来渲染。这也是目前通用玩法。
关键代码,拷贝属性值。
public PortalCardStyleQueryForPageResponse
createPortalCardStyleQueryForPageResponse(CardStyleQueryForPageResponse cardStyleQueryForPageResponse) {
PortalCardStyleQueryForPageResponse portalCardStyleQueryForPageResponse =
new PortalCardStyleQueryForPageResponse();
// 这就是始作俑者
BeanUtils.copyProperties(cardStyleQueryForPageResponse, portalCardStyleQueryForPageResponse);
return portalCardStyleQueryForPageResponse;
}
BeanUtils.copyProperties(cardStyleQueryForPageResponse, portalCardStyleQueryForPageResponse);作用之后,可以拷贝并拿到属性值,可见item字段是有值的。
登录机器查看spring全家桶使用版本,也都是5.2.19.RELEASE
故事没有高潮,直接End
一切都变了,我被现实重重抽了巴掌。
【活在当下】5.3.14版本
业务场景没有变,代码也没有变,但是结果却变了。
BeanUtils.copyProperties(cardStyleQueryForPageResponse, portalCardStyleQueryForPageResponse);作用之后,除了字段item=null外,其他字段都有值。
TM就是这么方,当时的感觉就是,一直使用的代码怎么就出问题了呢?遵循的价值观顷刻间崩塌,进入吃瓜状态。
祸从何来
就是从升级spring 5.3.14开始,厄运如影随行,像一颗定时炸弹,备不住什么时候就“哐当”一声。这次就把我给炸懵了。
好在哥们自愈能力极强,立马复盘。
翻看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);
}
}
}
}
}
}
}
如上面代码,自添加注释可以看出:
类属性有泛型且source和target类型不一致时,直接跳过,不再进行属性拷贝。
放到我们的业务上来回放,明显可以看出:
-
sourceBean是
CardStyleQueryForPageResponse类,其中有个属性List<CardStyleAttr> items是list<T>的泛型。 -
targetBean是
PortalCardStyleQueryForPageResponse类,其中也有个属性List<PortalCardStyleGetResponse> items也是List<T>的泛型。 -
且二者的泛型的具体类又不一致,sourceBean使用
CardStyleAttr、targetBean使用PortalCardStyleGetResponse;如此,正好与源码所述一致,即被忽略,不再赋值。
而在未升级之前的5.2.19.RELEASE版本,源码中其实并没有校验泛型是否一致这么一步。所以在对于未升级spring之前的业务版本,也一直是岁月静好。
我来算一算,怎么逢祸化吉?
BeanUtils.copyProperties可以用来作为Bean之间拷贝的快捷方法,但是对于一些本身Bean属性就很复杂的,还是不建议使用。
再者说了,源码大家也看到了,用到了反射取targetBean的setter、取sourceBean的getter来完成拷贝动作。这种方式相较于咱们直接手动写get/set完成赋值动作,性能消耗更大。
其实,我本人还是愿意手写get/set的,一者代码可阅读性更好,二者也花费不了太多时间。