今天想写一篇文章,主要关于深拷贝和浅拷贝相关的,主要是最近写代码的时候遇到一个BUG,刚好涉及到浅拷贝导致的问题。
问题背景
现在有一个需要是需要修改门店信息,门店也区分父门店和子门店,父门店被编辑更新是需要通过到第三方的,然后之前是没有父子门店的概念的,后面新增的需求,然后editShop这个方法的入参就是关于门店的信息么,这里我简化它的参数,但是保留了一个data属于引用型参数。
下面的模拟当时出现BUG的代码
public class RequestDto {
Map<String, Object> data = new HashMap<>();
private Integer status ;
private Long id;
public Map<String, Object> getData() {
return data;
}
}
public class ShopService {
public void editShop(RequestDto requestDto) {
System.out.println("入参:" + requestDto.toString());
// 操作
// 父门店获取子门店,假设有两个
List<Long> childShops = getChildShop(requestDto.getId());
childShops.stream().forEach(childShop -> {
RequestDto requestDto1 = new RequestDto();
BeanUtils.copyProperties(requestDto, requestDto1);
requestDto1.setId(childShop);
requestDto1.getData().put("isSync", Boolean.FALSE);
editShop(requestDto1);
sync(childShop);
});
System.out.println("返回前:" + requestDto.toString());
if (Boolean.FALSE.equals(requestDto.getData().get("isSync"))) {
return;
}
sync(requestDto.getId());
}
private List<Long> getChildShop(Long parentId) {
if (parentId.equals(1L)) {
// 父门店
List<Long> childShopIds = new ArrayList<>();
childShopIds.add(123L);
childShopIds.add(234L);
return childShopIds;
} else {
return new ArrayList<>();
}
}
private void sync(Long shopId) {
System.out.println("同步门店第三方:" + shopId);
}
}
public class Main {
public static void main(String[] args) {
ShopService shopService = new ShopService();
RequestDto requestDto = new RequestDto();
requestDto.setId(1L);
requestDto.setStatus(0);
shopService.editShop(requestDto);
}
}
模拟了当时操作父门店时,需要触发子门店的配置信息同步,通过递归来实现似乎是不错的选择,但是将isSync作为递归结束的标志来,然后就可以完成配置的复制和更新,看起来是没有什么问题的,但是当这个代码真的到达线上的时候,发现编辑父门店,子门店触发通过不了,但是父门店就触发不了同步了,在判断的if里面就退出了,导致线上发现父门店配置没有同步的BUG。然后经过一顿头发输出,看了好几遍日志之后,也没有发现问题的所在,后面本地起了一个调试才发现,这个问题是出自于BeanUtils.copyProperties的问题,排查出来了。
BeanUtils.copyProperties的时候,已经将requestDto1中data的引用指向了requestDto的引用,然后对它添加isSync的时候是会导致父门店的dto也同时被修改了。
BeanUtils.copyProperties的源码分析
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;
}
// 获取所有的属性
PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);
List<String> ignoreList = (ignoreProperties != null ? Arrays.asList(ignoreProperties) : null);
// 遍历
for (PropertyDescriptor targetPd : targetPds) {
// 写入的set方法
Method writeMethod = targetPd.getWriteMethod();
if (writeMethod != null && (ignoreList == null || !ignoreList.contains(targetPd.getName()))) {
// source对象对应的的属性
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);
// 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);
}
}
}
}
}
}
}
然后这里我们可以看到它最后也是通过反射的invoke方法来进行set设置值,而value是从之前的source获取,所以获取的是它的引用,所以用的是浅引用。
浅拷贝和深拷贝
浅拷贝从上面的例子应该是非常容易就可以理解了,就是浅拷贝除了拷贝基础数据类型及其包装类型外,在对引用类型的拷贝时是直接拷引用,在Java中就是相当于是同一个的对象的引用。
深拷贝的话就是相当于创建一个新的一模一样的对象,但是这个对象的内存地址是不一致的。
在Java里面实现深拷贝的方式有三种。
-
通过直接创建赋值,set
-
通过序列化和反序列化创建的对象是属于重新生成的对象,也是属于深拷贝。
-
通过fastjson这样的json反序列化也是实现深拷贝的一种方式。
-
通过实现重写父类的clone方法也可以实现。
借此记录下自己踩到的BUG,虽然不是很难的东西,写出来也是希望下一次遇到这个问题的时候能够更快的解决!