前言
由于项目中两个对象之间需要拷贝的属性较多,使用 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; private Integer weight; }
@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"); 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) { 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; }
public static void main(String[] args) { Source source = new Source(24, true); Target target = new Target(); BeanUtils.copyProperties(source, target); System.out.println(target); } }
|
运行后发现,int
与 Integer
类型的同名字段能够成功拷贝,而 boolean
和 Boolean
类型的同名属性是拷贝失败的:
当将上文代码中 Target
类的属性名 isVip
改为 vip
后,布尔类型属性值能够成功由基本数据类型拷贝到包装类型。
参考文章
51COT 博客 - copyProperties 复制对象 null 不变 copyproperties没有复制
CSDN - 如何解决BeanUtils.copyProperties方法覆盖字段为null,看这一篇就够了~~
CSDN - 为啥不建议用BeanUtils.copyProperties拷贝数据?