前言

由于项目中两个对象之间需要拷贝的属性较多,使用 getter/setter 方法难免会增加代码量与可读性,因此偷懒使用了 BeanUtils.copyProperties() 方法,结果碰巧就掉到了坑里,null 值被拷贝了……因此,特意查找网上的资料找到了对应的解决方法,也总结了该方法的几个容易掉进去的坑。

当然,更好的方法还是乖乖使用 getter/setter 方法吧……

一、属性名称或类型不一致无法拷贝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Attribute {

@Data
@AllArgsConstructor
static class Source {
private String userName; // 与Target类属性名称不一致
private Integer weight; // 与Target类属性类型不一致
}

@Data
static class Target {
private String nickName;
private Double weight;
}

public static void main(String[] args) {
Source source = new Source("loong", 120);
Target target = new Target();
BeanUtils.copyProperties(source, target);
System.out.println(target);
}
}

运行后,可以发现属性名称或者属性不同都会导致 copyProperties() 方法无法成功拷贝:

运行结果

二、内部类无法拷贝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class InternalClass {

@Data
@AllArgsConstructor
@NoArgsConstructor
static class Source {
private Long id;
private UserInfo user;

@Data
@AllArgsConstructor
static class UserInfo {
private String userName;
}
}

@Data
@AllArgsConstructor
@NoArgsConstructor
static class Target {
private Long id;
private UserInfo user;

@Data
@AllArgsConstructor
static class UserInfo {
private String userName;
}
}

public static void main(String[] args) {
Source.UserInfo userInfo = new Source.UserInfo("loong");
Source source = new Source(100L, userInfo);
Target target = new Target();
BeanUtils.copyProperties(source, target);
System.out.println(target);
}
}

运行后,可以发现内部类即使名称相同、类内属性也完全相同,还是无法使用 copyProperties() 方法进行拷贝:

运行结果

三、浅拷贝

深拷贝(Deep Copy)会创建一个新对象,并递归地复制对象内部的所有嵌套对象。这样,原对象和拷贝对象之间就没有任何共享的引用。同时,对深拷贝对象的修改不会影响到原对象。

而浅拷贝(Shallow Copy)会创建一个新对象,但不递归地复制对象内部的嵌套对象。也就是说,浅拷贝只复制对象本身及其直接包含的引用。如果原对象包含可变对象(如列表、字典等),那么对这些可变对象的修改会影响到原对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class ShallowCopy {

@Data
@AllArgsConstructor
@NoArgsConstructor
static class UserInfo {
private String userName;
private Inner inner;
}

@Data
@AllArgsConstructor
static class Inner{
private String innerName;
}

public static void main(String[] args) {
UserInfo source = new UserInfo("loong", new Inner("ok"));
UserInfo target = new UserInfo();
BeanUtils.copyProperties(source, target);
source.getInner().setInnerName("not ok"); // 当source修改后,target也会修改
System.out.println("source:" + source + "\n" +
"target:" + target);
}
}

运行后,发现目标对象进行数据拷贝后,当源对象的引用类型数据被修改了,目标对象中的该引用类型数据也会同时修改,这可能在不经意间造成错误:

运行结果

四、null 值覆盖导致异常

在两个有较多相同属性的对象需要进行数据拷贝时,为了减少代码量,很容易会想到使用 copyProperties() 方法,但我们实际开发中往往是不需要拷贝 null 值的,而 copyProperties() 方法偏偏就是任性,会把 null 值也原封不动地照搬。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class NullAttribute {

@Data
@AllArgsConstructor
static class UserInfo {
private String userName;
private Integer weight;
}

public static void main(String[] args) {
UserInfo source = new UserInfo("loong", null);
UserInfo target = new UserInfo("leaf", 120);
BeanUtils.copyProperties(source, target);
System.out.println(target);
}
}

以上段代码为例,我只是想让源对象的 userName 属性拷贝给目标对象,但是运行后,目标对象的 weight 属性也变为了 null

运行结果

那么如何避免这种情况发生呢?我们可以查看 copyProperties() 方法的源码,发现有多个重载方法,除了经常使用到的 public static void copyProperties(Object source, Object target) ,还有 public static void copyProperties(Object source, Object target, String... ignoreProperties) ,其中形参 ignoreProperties 即为需要忽略的属性名称数组

1
2
3
public static void copyProperties(Object source, Object target, String... ignoreProperties) throws BeansException {
copyProperties(source, target, null, ignoreProperties);
}

按照如下代码所示,即可使得 null 值并不会从源对象拷贝至目标对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static String[] getNullPropertyNames (Object source) {  // 得到 null 值属性名列表
final BeanWrapper src = new BeanWrapperImpl(source);
java.beans.PropertyDescriptor[] pds = src.getPropertyDescriptors();

Set<String> emptyNames = new HashSet<>();
for(java.beans.PropertyDescriptor pd : pds) {
Object srcValue = src.getPropertyValue(pd.getName());
if (srcValue == null) emptyNames.add(pd.getName());
}
String[] result = new String[emptyNames.size()];
return emptyNames.toArray(result);
}

BeanUtils.copyProperties(source, target, getNullPropertyNames(source)); // 完成属性拷贝

五、性能问题

copyProperties() 方法通常使用反射来访问源对象和目标对象的字段。使用反射机制访问和修改对象的字段会带来一定的性能开销。相较于直接访问对象属性,反射的速度通常较慢,因为它涉及到动态查找和访问权限检查。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Performance {

@Data
@AllArgsConstructor
@NoArgsConstructor
static class UserInfo {
private String userName;
}

public static void main(String[] args) {
UserInfo source = new UserInfo("loong");
UserInfo target = new UserInfo();
long start = System.currentTimeMillis();
for(int i = 0; i < 10000; i++) {
BeanUtils.copyProperties(source, target);
}
System.out.println("copyProperties():" + (System.currentTimeMillis() - start));

start = System.currentTimeMillis();
for(int i = 0; i < 10000; i++) {
target.setUserName(source.getUserName());
}
System.out.println("getter/setter:" + (System.currentTimeMillis() - start));
}
}

运行后发现,使用 getter/setter 方法进行属性拷贝的速度远快于使用 copyProperties() 方法的速度。并且经测试,即使需要拷贝的属性很多,copyProperties() 方法在性能上也依旧较差(但大家使用该方法不都是为了方便,不容易漏掉一些属性吗)。

运行结果

六、布尔类型的属性拷贝

该部分照应《阿里巴巴 Java 开发手册》的《编码规约内容》中第一部分第 8 条。

许多框架(如 Hibernate、Jackson)在处理 Boolean 类型时,依赖于 Java Bean 规范。如果属性使用了不符合规范的 isXxx() 方法,可能会导致以下问题:

  • 序列化和反序列化时,属性无法正确映射。
  • 数据库 ORM 框架(如 Hibernate)无法映射到对应的数据库字段。

因此,为了保证框架和工具的正常使用,推荐遵循 Java Bean 规范,避免为 Boolean 属性添加 is 前缀。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class CopyBoolean {

@Data
@AllArgsConstructor
static class Source {
private int age;
private boolean isVip;
}

@Data
static class Target {
private Integer age;
private Boolean isVip; // 改为 private Boolean vip; 能够成功拷贝
}

public static void main(String[] args) {
Source source = new Source(24, true);
Target target = new Target();
BeanUtils.copyProperties(source, target);
System.out.println(target);
}
}

运行后发现,intInteger 类型的同名字段能够成功拷贝,而 booleanBoolean 类型的同名属性是拷贝失败的:

运行结果

当将上文代码中 Target 类的属性名 isVip 改为 vip 后,布尔类型属性值能够成功由基本数据类型拷贝到包装类型。

参考文章

51COT 博客 - copyProperties 复制对象 null 不变 copyproperties没有复制

CSDN - 如何解决BeanUtils.copyProperties方法覆盖字段为null,看这一篇就够了~~

CSDN - 为啥不建议用BeanUtils.copyProperties拷贝数据?