今天线上某个接口返回的时间没有进行格式化了,导致时间显示出现问题。排查的时候发现是某个同事使用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之后会对泛型进行校验,如果泛型不一致则会直接忽略赋值。