在實際的業務開發中,我們經常會碰到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複製資料。文中不足之處,歡迎補充和指正。
作者:京東科技 孫揚威
來源:京東雲開發者社群 轉載請註明來源