在實際的業務開發中,我們經常會碰到VO、BO、PO、DTO等物件屬性之間的賦值,當屬性較多的時候我們使用get,set的方式進行賦值的工作量相對較大,因此很多人會選擇使用spring提供的拷貝工具BeanUtils的copyProperties方法完成物件之間屬性的拷貝。通過這種方式可以很大程度上降低我們手動編寫物件屬性賦值程式碼的工作量,既然它那麼方便為什麼還不建議使用呢?下面是我整理的BeanUtils.copyProperties資料拷貝一些常見的坑。
這個坑可以細分為如下兩種:
(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,也會導致拷貝失敗。
在業務開發時,我們可能會有部分欄位拷貝的需求,被拷貝的資料裡面如果某些欄位有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 拷貝資料的意義,因此也不推薦這麼做。
在使用 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
在開發或者排查問題過程中,如果我們在鏈路中查詢某個欄位值(呼叫方並未傳遞)的來源,我們可能會通過全文搜尋的方式,去找它對應的賦值方法(例如set方式、build方式等),但是如果在鏈路中使用BeanUtils.copyProperties拷貝了資料,就很難快速定位到賦值的地方,導致排查效率較低。
內部類資料無法正常拷貝,及時型別和欄位名均相同也無法拷貝成功,如下所示:
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會認為屬性不同,因此不會拷貝資料。
這裡我先給大家複習一下深拷貝和淺拷貝。
淺拷貝是指建立一個新物件,該物件的屬性值與原始物件相同,但對於參照型別的屬性,仍然共用相同的參照。也就是說在淺拷貝下,當原始內容的參照屬性值發生變化時,被拷貝物件的參照屬性值也會隨之發生變化。
深拷貝是指建立一個新物件,該物件的屬性值與原始物件相同,包括參照型別的屬性。深拷貝會遞迴複製參照物件,建立全新的物件,所以深拷貝拷貝後的物件與原始物件完全獨立。
下面是對應的程式碼範例:
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;
}
下面是執行結果:
總結:通過程式碼執行結果我們可以發現,一旦你在拷貝後修改了原始物件的參照型別的資料,就會導致拷貝資料的值發生異常,這種問題排查起來也比較困難。
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拷貝資料。文中不足之處,歡迎補充和指正。
作者:京東科技 孫揚威
來源:京東雲開發者社群 轉載請註明來源