前语

咱们好,我是田螺

咱们日常开发中,常常涉及到DO、DTO、VO方针特点仿制赋值,很容易想到org.springframework.beans.BeanUtilscopyProperties 。它会主动通过反射机制获取源方针和方针方针的特点,并将对应的特点值进行仿制。能够削减手动编写特点仿制代码的工作量,进步代码的可读性和维护性。

可是你知道嘛?运用BeanUtilscopyProperties ,会有好几个坑呢,今日田螺哥个咱们盘点一下哈:

BeanUtils.copyProperties的11个坑

  • 大众号捡田螺的小男孩 (有田螺精心原创的面试PDF)
  • github地址,感谢每颗star:github

第1个坑: 类型不匹配

@Data
public class SourceBean {
    private Long age;
}
@Data
public class TargetBean {
    private String age;
}
public class Test {
    public static void main(String[] args) {
        SourceBean source = new SourceBean();
        source.setAge(25L);
        TargetBean target = new TargetBean();
        BeanUtils.copyProperties(source, target);
        System.out.println(target.getAge());  //仿制赋值失利,输出null
    }
}

在上述demo中,源方针SourceBeanage特点是一个Long类型,而方针方针TargetBeanage特点是一个String类型。因为类型不匹配,BeanUtils.copyProperties不会赋值成功的。我跑demo的成果,操控台输出null

第2个坑: BeanUtils.copyProperties是浅仿制

先给咱们温习一下,什么是深仿制?什么是浅仿制?

  • 浅仿制是指创建一个新方针,该方针的特点值与原始方针相同,但关于引证类型的特点,仍然共享相同的引证。换句话说,浅仿制只仿制方针及其引证,而不仿制引证指向的方针本身。
  • 深仿制是指创建一个新方针,该方针的特点值与原始方针相同,包括引证类型的特点。深仿制会递归仿制引证方针,创建全新的方针,以确保仿制后的方针与原始方针完全独立

BeanUtils.copyProperties的11个坑

我再给个代码demo给咱们看看哈:

public class Address {
    private String city;
    //getter 和 setter 办法省掉
}
public class Person {
    private String name;
    private Address address;
    //getter 和 setter 办法省掉
}
 Person sourcePerson = new Person();
 sourcePerson.setName("John");
 Address address = new Address();
 address.setCity("New York");
 sourcePerson.setAddress(address);
 Person targetPerson = new Person();
 BeanUtils.copyProperties(sourcePerson, targetPerson);
 sourcePerson.getAddress().setCity("London");
 System.out.println(targetPerson.getAddress().getCity());  // 输出为 "London"

在上述示例中,源方针Person的特点address是一个引证类型。当运用BeanUtils.copyProperties办法进行特点仿制时,实际上只仿制了引证,即方针方针targetPersonaddress 特点引证和源方针 sourcePersonaddress 特点引证指向同一个方针。因而,当修正源方针的address方针时,方针方针的address方针也会被修正。

咱们日常开发中,要留意这个坑哈~

第3个坑:特点称号不一致

 public class SourceBean {
    private String username;
    // getter 和 setter 办法省掉
}
public class TargetBean {
    private String userName;
    // getter 和 setter 办法省掉
}
 SourceBean source = new SourceBean();
 source.setUsername("捡田螺的小男孩");
 TargetBean target = new TargetBean();
 BeanUtils.copyProperties(source, target);
 System.out.println(target.getUserName());   // 输出为 null

在上述示例中,源方针SourceBean 的特点称号是username,而方针方针TargetBean的特点称号也是userName可是,两个 username,一个N是大写,一个n是小写,即特点称号不一致BeanUtils.copyProperties办法无法主动映射这些特点(无法疏忽大小写主动匹配),因而方针方针的userName特点值为null

咱们日常开发中,要留意这个坑哈~ 比方大小写不一致,差一两个字母等等

第4个坑:Null 值掩盖

@Data
public class SourceBean {
    private String name;
    private String address;
}
@Data
public class TargetBean {
    private String name;
    private String address;
}
SourceBean source = new SourceBean();
source.setName("John");
source.setAddress(null);
TargetBean target = new TargetBean();
target.setAddress("田螺address");
BeanUtils.copyProperties(source, target);
System.out.println(target.getAddress());  // 输出为 null

在上述示例中,源方针 SourceBean 的 address 特点值为 null。默认情况下,BeanUtils.copyProperties 办法会将源方针中的 null 值特点掩盖到方针方针中。因而,方针方针的 address 特点值也为 null。

假如你不希望 null 值掩盖方针方针中的特点,能够运用 BeanUtils.copyProperties 办法的重载办法,并传入一个自定义的 ConvertUtilsBean 实例来进行装备。

第5个坑:留意引入的包

BeanUtils.copyProperties其实有两个包,分别是spring、apache。咱们留意一下哈,这两个包,是有点不一样的:

//org.springframework.beans.BeanUtils(源方针在左面,方针方针在右边)
public static void copyProperties(Object source, Object target) throws BeansException 
//org.apache.commons.beanutils.BeanUtils(源方针在右边,方针方针在左面)
public static void copyProperties(Object dest, Object orig) throws IllegalAccessException, InvocationTargetException

咱们运用的时分,要留意一下哈,千万留意自己引入的哪个BeanUtils,写对应参数位置。

第6个坑:Boolean类型数据+is特点最初的坑

SourceBean和TargetBean中的都有个特点isTianLuo,它们的数据类型坚持不变,可是一个为基本类型boolean,一个为包装类型Boolean

@Data
public class SourceBean {
    private boolean isTianLuo;
}
@Data
public class TargetBean {
    private Boolean isTianLuo;
}

跑测试用里的时分,发现赋值不上:

SourceBean source = new SourceBean();
source.setTianLuo(true);
TargetBean target = new TargetBean();
BeanUtils.copyProperties(source, target);
System.out.println(target.getIsTianLuo()); // 输出为 null

为什么呢?即使是一个包装类型,一个基本类型,应该能够赋值上才对的。

这是因为当特点类型为boolean时,特点名以is最初,特点名会去掉前面的is,因而源方针和方针方针特点对不上啦。

咱们运用BeanUtils.copyProperties过程中,要留意哈~

第7个坑:查找不到字段引证

在某些开发场景呢,假如咱们要修正某个字段的赋值,咱们或许会全文查找它的一切set办法,看哪些地方引证到。

BeanUtils.copyProperties的11个坑

可是呢,假如运用BeanUtils.copyProperties就不知道是否引证到对应的ste办法啦即查找不到字段引证。这就或许导致你会漏掉修正对应的字段。

BeanUtils.copyProperties的11个坑

第8个坑:不同内部类,即使相同特点,也是赋值失利

@Data
public class CopySource {
    public String outerName;
    public CopySource.InnerClass innerClass;
    @Data
    public static class InnerClass {
        public String InnerName;
    }
}
@Data
public class CopyTarget {
    public String outerName;
    public CopyTarget.InnerClass innerClass;
    @Data
   public static class InnerClass {
        public String InnerName;
    }
}
CopySource test1 = new CopySource();
test1.outerName = "outTianluo";
CopySource.InnerClass innerClass = new CopySource.InnerClass();
innerClass.InnerName = "innerTianLuo";
test1.innerClass = innerClass;
System.out.println(test1);
CopyTarget test2 = new CopyTarget();
BeanUtils.copyProperties(test1, test2);
System.out.println(test2);  //输出CopyTarget(outerName=outTianluo, innerClass=null)

以上demo中,CopySourceCopyTarget各自存在一个内部类InnerClass,虽然这个内部类特点也相同,类名也相同,可是在不同的类中,因而Spring会认为特点不同,不会Copy;

假如要仿制成功,能够让他们指向同一个内部类。

第9个坑:bean对应的特点,没有getter和setter办法,赋值失利

BeanUtils.copyProperties要仿制特点值成功,需求对应的bean要有getter和setter办法。因为它是用反射拿到set和get办法再去拿特点值和设置特点值的。

@Data
public class SourceBean {
    private String value;
}
@Getter   //没有对应的setter办法
public class TargetBean {
    private String value;
}
SourceBean source = new SourceBean();
source.setValue("捡田螺的小男孩");
TargetBean target = new TargetBean();
BeanUtils.copyProperties(source, target);
System.out.println(target.getValue()); //输出null 

第10个坑:BeanUtils.copyProperties + 泛型

假如BeanUtils.copyProperties遇到泛型,也是很或许赋值失利的哈。咱们看下这个例子:

@Data
public class CopySource {
    public String outerName;
    public List<CopySource.InnerClass> clazz;
    @Data
    public static class InnerClass {
        public String InnerName;
    }
}
@ToString
@Data
public class CopyTarget {
    public String outerName;
    public List<CopyTarget.InnerClass> clazz;
    @Data
    public static class InnerClass {
        public String InnerName;
    }
}
CopySource test1 = new CopySource();
test1.outerName = "outTianluo";
CopySource.InnerClass innerClass = new CopySource.InnerClass();
innerClass.InnerName = "innerTianLuo";
List<CopySource.InnerClass> clazz = new ArrayList<>();
clazz.add(innerClass);
test1.setClazz(clazz);
System.out.println(test1);
CopyTarget test2 = new CopyTarget();
BeanUtils.copyProperties(test1, test2);
System.out.println(test2);  //输出CopyTarget(outerName=outTianluo, clazz=null)

这里面的例子,BeanUtils.copyProperties办法仿制包括泛型特点的方针clazzCopyTargetCopySource的泛型特点类型不匹配,因而仿制赋值失利。

假如是低版本的包,泛型假如不匹配,则会报错,高本版则知识仿制赋值失利。

第11个坑:功能问题

因为这些BeanUtils类都是选用反射机制完成的,对程序的效率也会有影响。我跑了个demo对比:

SourceBean sourceBean = new SourceBean();
sourceBean.setName("tianLuoBoy");
TargetBean target = new TargetBean();
long beginTime = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {  //循环10万次
      target.setName(sourceBean.getName());
}
System.out.println("common setter time:" + (System.currentTimeMillis() - beginTime));
long beginTime1 = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {  //循环10万次
    BeanUtils.copyProperties(sourceBean, target);
}
System.out.println("bean copy time:" + (System.currentTimeMillis() - beginTime1));
//输出
common setter time:3
bean copy time:331

能够发现,简单的setterBeanUtils.copyProperties对比,功能差距非常大。因而,慎用BeanUtils.copyProperties!!!

12. 替换BeanUtils.copyProperties的计划

以上聊了BeanUtils.copyProperties的11个坑,都是在跟咱们聊,要慎用BeanUtils.copyProperties。那有没有推荐替换它的计划呢。

第一种,那就是运用原始的setter和getter办法。

运用手动的setter办法进行特点赋值。这种办法或许需求编写更多的代码,可是能够供给更细粒度的操控,并且在功能方面一般比BeanUtils.copyProperties更高效。

Target target = new Target();
target.setName(source.getName());
target.setAge(source.getAge());

假如实在方针bean的特点比较多的话,能够运用插件GenerateAllSetter,它能够一键生成方针的set办法,挺方便的。

BeanUtils.copyProperties的11个坑

第二种计划,运用映射工具库,如MapStruct、ModelMapper等,它们能够主动生成特点映射的代码。这些工具库能够削减手动编写setter办法的工作量,并供给更好的功能。

运用MapStruct的示例:

@Mapper
public interface SourceTargetMapper {
    SourceTargetMapper INSTANCE = Mappers.getMapper(SourceTargetMapper.class);
    @Mapping(source = "name", target = "name")
    @Mapping(source = "age", target = "age")
    Target mapToTarget(Source source);
}
Target target = SourceTargetMapper.INSTANCE.mapToTarget(source);
觉得有收成的伙伴,给个三连支持一下哈,感谢~