在實際的業務開發中,我們經常會碰到VO、BO、PO、DTO等對象屬性之間的賦值,當屬性較多的時候我們使用get,set的方式進行賦值的工作量相對較大,因此很多人會選擇使用spring提供的拷貝工具BeanUtils的copyProperties方法完成對象之間屬性的拷貝。通過這種方式可以很大程度上降... ...
在實際的業務開發中,我們經常會碰到VO、BO、PO、DTO等對象屬性之間的賦值,當屬性較多的時候我們使用get,set的方式進行賦值的工作量相對較大,因此很多人會選擇使用spring提供的拷貝工具BeanUtils的copyProperties方法完成對象之間屬性的拷貝。通過這種方式可以很大程度上降低我們手動編寫對象屬性賦值代碼的工作量,既然它那麼方便為什麼還不建議使用呢?下麵是我整理的BeanUtils.copyProperties數據拷貝一些常見的坑。
1:屬性類型不一致導致拷貝失敗
這個坑可以細分為如下兩種:
(1)同一屬性的類型不同
在實際開發中,很可能會出現同一欄位在不同的類中定義的類型不一致,例如ID,可能在A類中定義的類型為Long,在B類中定義的類型為String,此時如果使用BeanUtils.copyProperties進行拷貝,就會出現拷貝失敗的現象,導致對應的欄位為null,對應案例如下:
public class BeanUtilsTest {
public static void main(String[] args) {
SourcePoJo sourcePoJo = new SourcePoJo("jingdong", (long) 35711);
TargetPoJo targetPoJo = new TargetPoJo();
BeanUtils.copyProperties(sourcePoJo,targetPoJo);
System.out.println(targetPoJo);
}
}
@Data
@AllArgsConstructor
class SourcePoJo{
private String username;
private Long id;
}
@Data
class TargetPoJo{
private String username;
private String id;
}
對應的運行結果如下:
可以看到id欄位由於類型不一致,導致拷貝後的值為null。
(2)同一欄位分別使用包裝類型和基本類型
如果通一個欄位分別使用包裝類和基本類型,在沒有傳遞實際值的時候,會出現異常,具體案例如下:
public class BeanUtilsTest {
public static void main(String[] args) {
SourcePoJo sourcePoJo = new SourcePoJo();
sourcePoJo.setUsername("joy");
TargetPoJo targetPoJo = new TargetPoJo();
BeanUtils.copyProperties(sourcePoJo,targetPoJo);
System.out.println(targetPoJo);
}
}
@Data
class SourcePoJo{
private String username;
private Long id;
}
@Data
class TargetPoJo{
private String username;
private long id;
}
在測試案例中,id欄位在拷貝源和拷貝目標中分別使用包裝類型和基本類型,可以看到下麵在拷貝時出現了異常。
註意:如果一個布爾類型的屬性分別使用了基本類型和包裝類型,且屬性名如果使用is開頭,例如isSuccess,也會導致拷貝失敗。
2:null值覆蓋導致數據異常
在業務開發時,我們可能會有部分欄位拷貝的需求,被拷貝的數據裡面如果某些欄位有null值存在,但是對應的需要被拷貝過去的數據的相同欄位的值並不為null,如果直接使用 BeanUtils.copyProperties 進行數據拷貝,就會出現被拷貝數據的null值覆蓋拷貝目標數據的欄位,導致原有的數據失效。
對應的案例如下:
public class BeanUtilsTest {
public static void main(String[] args) {
SourcePoJo sourcePoJo = new SourcePoJo();
sourcePoJo.setId("35711");
TargetPoJo targetPoJo = new TargetPoJo();
targetPoJo.setUsername("Joy");
BeanUtils.copyProperties(sourcePoJo,targetPoJo);
System.out.println(targetPoJo);
}
}
@Data
class SourcePoJo{
private String username;
private String id;
}
@Data
class TargetPoJo{
private String username;
private String id;
}
對應的運行結果如下:
可以看到拷貝目標結果中原本有值的username欄位,它的值被覆蓋成了null。雖然可以使用 BeanUtils.copyProperties 的重載方法,配合自定義的 ConvertUtilsBean 來實現部分欄位的拷貝,但是這麼做本身也比較複雜,也就失去了使用BeanUtils.copyProperties 拷貝數據的意義,因此也不推薦這麼做。
3:導包錯誤導致拷貝數據異常
在使用 BeanUtils.copyProperties 拷貝數據時,如果項目中同時引入了Spring的beans包和Apache的beanutils包,在導包的時候,如果導入錯誤,很可能導致數據拷貝失敗,排查起來也不太好發現。我們通常使用的是Sping包中的拷貝方法,兩者的區別如下:
//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
4:查找不到欄位引用,修改內容難以溯源
在開發或者排查問題過程中,如果我們在鏈路中查找某個欄位值(調用方並未傳遞)的來源,我們可能會通過全文搜索的方式,去找它對應的賦值方法(例如set方式、build方式等),但是如果在鏈路中使用BeanUtils.copyProperties拷貝了數據,就很難快速定位到賦值的地方,導致排查效率較低。
5:內部類數據無法成功拷貝
內部類數據無法正常拷貝,及時類型和欄位名均相同也無法拷貝成功,如下所示:
public class BeanUtilsTest {
public static void main(String[] args) {
SourcePoJo sourcePoJo = new SourcePoJo();
sourcePoJo.setUsername("joy");
SourcePoJo.InnerClass innerClass = new SourcePoJo.InnerClass("sourceInner");
sourcePoJo.innerClass=innerClass;
System.out.println(sourcePoJo.toString());
TargetPoJo targetPoJo = new TargetPoJo();
BeanUtils.copyProperties(sourcePoJo,targetPoJo);
System.out.println(targetPoJo.toString());
}
}
//下麵是類的信息,這裡就直接放到一塊展示了
@Data
@ToString
public class SourcePoJo{
private String username;
private Long id;
public InnerClass innerClass;
@Data
@ToString
@AllArgsConstructor
public static class InnerClass{
public String innerName;
}
}
@Data
@ToString
public class TargetPoJo{
private String username;
private Long id;
public InnerClass innerClass;
@Data
@ToString
public static class InnerClass{
public String innerName;
}
}
下麵是運行結果:
上面案例中,在拷貝源和拷貝目標中各自存在一個內部類InnerClass,雖然這個內部類屬性也相同,類名也相同,但是在不同的類中,因此Spring會認為屬性不同,因此不會拷貝數據。
6:BeanUtils.copyProperties是淺拷貝
這裡我先給大家複習一下深拷貝和淺拷貝。
淺拷貝是指創建一個新對象,該對象的屬性值與原始對象相同,但對於引用類型的屬性,仍然共用相同的引用。也就是說在淺拷貝下,當原始內容的引用屬性值發生變化時,被拷貝對象的引用屬性值也會隨之發生變化。
深拷貝是指創建一個新對象,該對象的屬性值與原始對象相同,包括引用類型的屬性。深拷貝會遞歸複製引用對象,創建全新的對象,所以深拷貝拷貝後的對象與原始對象完全獨立。
下麵是對應的代碼示例:
public class BeanUtilsTest {
public static void main(String[] args) {
Person sourcePerson = new Person("sunyangwei",new Card("123456"));
Person targetPerson = new Person();
BeanUtils.copyProperties(sourcePerson, targetPerson);
sourcePerson.getCard().setNum("35711");
System.out.println(targetPerson);
}
}
@Data
@AllArgsConstructor
class Card {
private String num;
}
@NoArgsConstructor
@AllArgsConstructor
@Data
class Person {
private String name;
private Card card;
}
下麵是運行結果:
總結:通過代碼運行結果我們可以發現,一旦你在拷貝後修改了原始對象的引用類型的數據,就會導致拷貝數據的值發生異常,這種問題排查起來也比較困難。
7:底層實現為反射拷貝效率低
BeanUtils.copyProperties底層是通過反射獲取到對象的set和get方法,然後通過get、set完成數據的拷貝,整體拷貝效率較低。
下麵是使用BeanUtils.copyProperties拷貝數據和直接set的方式賦值效率對比,為了便於直觀的看出效果,這裡以拷貝1萬次為例:
public class BeanUtilsTest {
public static void main(String[] args) {
long copyStartTime = System.currentTimeMillis();
User sourceUser = new User("sunyangwei");
User targetUser = new User();
for(int i = 0; i < 10000; i++) {
BeanUtils.copyProperties(sourceUser, targetUser);
}
System.out.println("copy方式:"+(System.currentTimeMillis()-copyStartTime));
long setStartTime = System.currentTimeMillis();
for(int i = 0; i < 10000; i++) {
targetUser.setUserName(sourceUser.getUserName());
}
System.out.println("set方式:"+(System.currentTimeMillis()-setStartTime));
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
class User{
private String userName;
}
下麵是執行的效率結果對比:
可以發現,常規的set和BeanUtils.copyProperties對比,性能差距非常大。因此,慎用BeanUtils.copyProperties。
以上就是在使用BeanUtils.copyProperties拷貝數據時常見的坑,這些坑大多都是比較隱蔽的,出了問題不太好排查,因此不建議在業務中使用BeanUtils.copyProperties拷貝數據。文中不足之處,歡迎補充和指正。
作者:京東科技 孫揚威
來源:京東雲開發者社區 轉載請註明來源