BeanUtils.copyProperties浅拷贝的坑你得知道?
  r4jK8vrjPuLR 2023年11月19日 30 0


今天想写一篇文章,主要关于深拷贝和浅拷贝相关的,主要是最近写代码的时候遇到一个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浅拷贝的坑你得知道?_List

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,虽然不是很难的东西,写出来也是希望下一次遇到这个问题的时候能够更快的解决!


【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2023年11月19日 0

暂无评论

推荐阅读
r4jK8vrjPuLR