Spring-BeanUtil使用copyProperties,却遭遇注解失效之谜!

今天线上某个接口返回的时间没有进行格式化了,导致时间显示出现问题。排查的时候发现是某个同事使用Beanutil对复杂对象进行复制,导致复制后的对象有问题。

场景还原

目标对象

java 复制代码
@Data
public class CostAdjustOrderListResult implements Serializable {

    private static final long serialVersionUID = -984768384423076174L;
    private List<CostAdjustOrderResult> list;
    private Integer total;

}
java 复制代码
@Data
public class CostAdjustOrderResult implements Serializable {
    private static final long serialVersionUID = -7512247411179094824L;
    private String adjustOrderSn;
    private String gsStoreName;
    private String creator;
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date createTime;

}

原对象

java 复制代码
public class CostAdjustOrderListResponse implements Serializable {
    private static final long serialVersionUID = 2668966399381103886L;
    private List<CostAdjustOrderResponse> list;
    private Integer total;
java 复制代码
@Data
public class CostAdjustOrderResponse implements Serializable {
    private static final long serialVersionUID = 934995811237387869L;
    private String adjustOrderSn;
    private String gsStoreName;
    private String creator;
    private Date createTime;
}

调用beanutil进行属性copy,这里的FsBeanUtil.map实际调用的就是BeanUtils.copyProperties

java 复制代码
   @Override
    public CostAdjustOrderListResult queryCostAdjustOrderSn(CostAdjustParam param) {
        CostAdjustRequest request = FsBeanUtil.map(param, CostAdjustRequest.class);
        request.setStoreIdList(getStoreIdListRequest(param));
        CostAdjustOrderListResponse response = costAdjustFacade.queryCostAdjustOrderSn(request);
        return FsBeanUtil.map(response, CostAdjustOrderListResult.class);
    }
java 复制代码
    public static <T> T map(Object source, Class<T> target) {
        if (null == source) {
            return null;
        } else {
            T t = BeanUtils.instantiateClass(target);
            BeanUtils.copyProperties(source, t);
            return t;
        }
    }

请求结果,createTime没有进行格式化

源码分析

其实看到上面的代码,其实就知道肯定有坑,复制的对象里有泛型,且泛型是个复杂对象,而且目标对象和源对象的泛型不一致,这些情况都很有风险。解决方案也很多,这里就不多赘述了。为什么会出现这个原因这次的重点。

点开copyProperties方法,代码还是挺长的。

java 复制代码
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对象
		Class<?> actualEditable = target.getClass();
                // 设置限制进行属性赋值的类,若不在其中则异常提示
		if (editable != null) {
			if (!editable.isInstance(target)) {
				。。。。抛异常
			}
			actualEditable = editable;
		}
        // 获取类的所有属性
		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 &&
							//判断源对象的是否可以赋值给目标对象,5.3之后增加了泛型判断,5.3之前只是对字段的类型进行判断,比如List<aaa>和List<bbb>在5.3之前由于不比较泛型,所以都是List也就能通过校验,       
                                                  //5.3之后由于加了泛型校验,所以这里会直接判断两个字段不相同,无法通过校验
							ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())) {
						try {
							// 如果不是公共方法,则设置setAccessible
							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);
						}
					}
				}
			}
		}
	}

点开writeMethod.invoke方法

java 复制代码
   @CallerSensitive
    public Object invoke(Object obj, Object... args)
        throws IllegalAccessException, IllegalArgumentException,
           InvocationTargetException
    {
		// 检查是否有方法的访问权限
        if (!override) {
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                Class<?> caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, obj, modifiers);
            }
        }
		// 获取MethodAccessor
        MethodAccessor ma = methodAccessor;             // read volatile
        if (ma == null) {
            ma = acquireMethodAccessor();
        }
        return ma.invoke(obj, args);
    }

点开ma.invoke方法会发现有三个实现,只用看NativeMethodAccessorImpl对象就可以了

java 复制代码
    NativeMethodAccessorImpl(Method var1) {
        // 目标对象的method对象
        this.method = var1;
    }

    public Object invoke(Object var1, Object[] var2) throws IllegalArgumentException, InvocationTargetException {
        if (++this.numInvocations > ReflectionFactory.inflationThreshold() && !ReflectUtil.isVMAnonymousClass(this.method.getDeclaringClass())) {
            MethodAccessorImpl var3 = (MethodAccessorImpl)(new MethodAccessorGenerator()).generateMethod(this.method.getDeclaringClass(), this.method.getName(), this.method.getParameterTypes(), this.method.getReturnType(), this.method.getExceptionTypes(), this.method.getModifiers());
            this.parent.setDelegate(var3);
        }

        return invoke0(this.method, var1, var2);
    }

这里就是赋值方法了,将源对象的属性赋值给目标对象。

流程追踪

当然看代码,其实可能还是不太直观,但是我们debug一下,在根据之前的代码就会十分清晰了

首先代码流转到writeMethod.invoke(target, value),要写入的方法是CostAdjustOrderResult.setList,他的泛型是 要读的方法是CostAdjustOrderResponse.getList

然后经过各种校验流转到NativeMethodAccessorImpl.invoke方法,var1是要目标对象,var2是源目标对象,然后你会发现他method对象的方法是CostAdjustOrderResult.setList且泛型是CostAdjustOrderResult,但是赋值的list对象却是CostAdjustOrderResponse

再执行一步,神奇的事出现了,CostAdjustOrderListResult.getList对象不是泛型指定的CostAdjustOrderResult而是CostAdjustOrderResponse

这时候结果就很明显了,拷贝的成员属性实际类型跟声明的不一致。

回到最开始的问题,接口返回的时间没有进行格式化,就有了解答,我们以为list返回会的对象是声明的CostAdjustOrderListResult实际上返回的是CostAdjustOrderResponse,而CostAdjustOrderResponse对象是没有@JSONFormat注解的,所以导致返回的时间没有格式化。

总结

所以当我们在复杂对象中使用Beanutil复制时,一定要注意泛型不一致的情况,不然会有可能导致拷贝的成员属性实际类型跟声明的类型不一致。但是这个情况目前只会出现在spring5.3之前的Beanutil中,5.3之后会对泛型进行校验,如果泛型不一致则会直接忽略赋值。

相关推荐
loop lee3 分钟前
Redis - Token & JWT 概念解析及双token实现分布式session存储实战
java·redis
ThetaarSofVenice4 分钟前
能省一点是一点 - 享元模式(Flyweight Pattern)
java·设计模式·享元模式
InSighT__6 分钟前
设计模式与游戏完美开发(2)
java·游戏·设计模式
神仙别闹6 分钟前
基于Java2D和Java3D实现的(GUI)图形编辑系统
java·开发语言·3d
dbcat官方11 分钟前
1.微服务灰度发布(方案设计)
java·数据库·分布式·微服务·中间件·架构
雪球不会消失了13 分钟前
SpringMVC中的拦截器
java·开发语言·前端
羊村懒哥21 分钟前
tomcat-安装笔记(包含虚拟主机配置)
java·笔记·tomcat
00Allen0023 分钟前
mybatis/mybatisplus
java·spring·mybatis
Echo flower25 分钟前
mybatis-plus自动填充时间的配置类实现
java·数据库·mybatis
程序员大阳27 分钟前
闲谭Scala(1)--简介
开发语言·后端·scala·特点·简介