那些年,那些对象拷贝的坑

590 阅读3分钟

最近开发了一个数据同步的功能,需要进行对象的拷贝,由于对象拷贝的工具的原理和细节不了解,踩了几个坑,在这里跟大家分享一下。

public  static  void  main(String[] args){
    
    SourceDTO  sourceDTO = new SourceDTO();
    sourceDTO.setSex("man");
    sourceDTO.setCreateTime("2022-02-12 12:00:00");
    SysNetworkDTO sysNetworkDTO = new SysNetworkDTO();
    List<SysNetworkDTO> list = Lists.newArrayList();
    list.add(sysNetworkDTO);
    //SourceDTO中的list中的数据为SysNetworkDTO类型
    sourceDTO.setList(list);
    TargetDTO  targetDTO = new TargetDTO();
    BeanUtils.copyProperties(sourceDTO,targetDTO);
    //TargetDTO中的list中的数据为SysNetworkVO类型
    if(CollectionUtils.isNotEmpty(targetDTO.getList())){
      for(SysNetworkVO sysNetworkVO:targetDTO.getList()){

      }
   }
}

代码执行到上面的第15行时,报错"main"
java.lang.ClassCastException: com.netty.use.nettyuse.copy.SysNetworkDTO cannot be cast to com.netty.use.nettyuse.copy.SysNetworkVO,类型转换错误

错误的根源:

BeanUtils工具是浅拷贝,对于对象中嵌套的List属性,指针还是指向原来的内存空间,并没有创建新的对象出来。对象targetDTO的list属性值,在拷贝的时候,只是把指针指向了List的内存空间,list里面数据本质上是SysNetworkDTO类型。所以我们循环遍历的时候,就出现了类型无法转化错误。

很多同学会说,那在测试环境没有测试出来这个问题吗,测试环境还真没这个问题。因为测试环境中,我的源数据是从数据中读取的,list属性值为null,而线上有部分数据list属性值非null,就导致上线后有值的那部分数据处理异常了。

下面看下BeanUtils和Orika的一些区别

1.原始对象和目标对象,属性名称相同,类型不同

原始类:

@Data
public class SourceDTO {

    private  String name;

    private  String  sex;

    private  String createTime;
  
    private  SysNetworkDTO  sysNetwork;

    private List<SysNetworkDTO> list;
}

目标类:

@Data
public class TargetDTO {
  
  private String name;
  
  private Integer age;
  
  private LocalDateTime createTime;
  
  private  SysNetworkVO  sysNetwork;
  
  private List<SysNetworkVO> list;
}

我们使用下面的代码测试下

SourceDTO source = new SourceDTO();
source.setName("李太白");
source.setSex("1k");
TargetDTO  targetDTO =  orikaBeanMapper.map(source, TargetDTO.class);
System.out.println(JsonUtils.toJson(targetDTO));

打印结果:{"name":"李太白"},虽然sex字段在两个类中不一致,拷贝并没有报错,值也没有拷贝上。

我们在看下给createTime字段赋值之后,再拷贝会是什么后果:

SourceDTO source = new SourceDTO();
source.setName("李太白");
source.setSex("1k");
source.setCreateTime("2022-12-22 09:09:09");
TargetDTO  targetDTO =  orikaBeanMapper.map(source, TargetDTO.class);
System.out.println(JsonUtils.toJson(targetDTO));

拷贝时报错:

ma.glasnost.orika.MappingException: No converter registered for conversion from String to LocalDateTime, nor any ObjectFactory which can generate LocalDateTime from String

我们再换成Spring的BeanUtils工具试试:

TargetDTO targetDTO = new TargetDTO();
BeanUtils.copyProperties(source, targetDTO);

打印结果:{"name":"李太白"},能够正常打印,类型不一致的属性值,不会拷贝。

那我们再看下嵌套对象的情况:

SourceDTO source = new SourceDTO();
source.setName("李太白");
source.setSex("1k");
List<SysNetworkDTO> list = Lists.newArrayList();
SysNetworkDTO sysNetworkDTO = new SysNetworkDTO();
sysNetworkDTO.setId(1);
sysNetworkDTO.setName("网络");
source.setSysNetwork(sysNetworkDTO);
list.add(sysNetworkDTO);
source.setList(list);
TargetDTO targetDTO = new TargetDTO();
BeanUtils.copyProperties(source, targetDTO);
System.out.println(JsonUtils.toJson(targetDTO));

TargetDTO targetDTO2 = orikaBeanMapper.map(source, TargetDTO.class);
System.out.println(JsonUtils.toJson(targetDTO2));
System.out.println(targetDTO2.getSysNetwork().getName());

BeanUtils打印结果:{"list":[{"id":1,"name":"网络"}],"name":"李太白"}

orika拷贝后的打印结果:{"list":[{"id":1,"name":"网络"}],"name":"李太白","sysNetwork":{"ref":"ref":".list[0]"}}

从打印结果,我们可以得出一个结论:BeanUtils对于属性名称相同,但是类型不同的属性,属性值拷贝不上,但不会报错。orika对于属性名称相同,但是类型不同的属性,会尝试拷贝,但可能会因为没有对应的转化器而报错。