關於Spring中的BeanUtils的使用的細節和由此導致的巨坑
前言
本文研究關於spring帶的BeanUtils的坑。
即 BeanUtils.copyProperties
的使用注意點
結論:
- 名字要相同
- 類型要相同(primitive type和對應包裝類同)
- Source必須有getter,且必須public
- Target必須有setter,且必須public
- Source中有static則不行(static和static final)
- Target中final的不會被賦值(final和static final)
- Source或Target中,若有父類,父類中的字段是可以複製的
- Source中內嵌的非普通類要單獨進行set(避免大坑)
使用注意點
假設有個Source類,和一個Target類
Source轉換到Target,一般除了基礎類型的字段(8種原始類型+對應的包裝類+String),如果包含有其他類型的,使用要十分謹慎!!!
- Source裏的字段名和Target裏的字段名,必須完全一樣,這個能夠轉換的第一個條件
- Source裏的字段類型和Target裏的要嚴格相同,這是第二個條件
- Integer 類型轉 Integer 是ok的
- int -> Integer (ok,原始類型和包裝類是互通的)
- Integer -> int (ok)
- int -> long (not ok,同是整數僅範圍不同,不行,必須嚴格相同類型)
- int -> Long (not ok)
- long -> int (not ok)
- long -> Integer (not ok)
- Dog -> Dog (ok)
- Animal -> Animal (ok)
- Dog -> Animal (not ok,即使是Dog繼承Animal,不行,必須嚴格相同的類型)
- Animal -> Dog (not ok)
- List -> List (ok,非常大的坑,及其容易在後續使用時發生ClassCastException,參考後續的附錄中的代碼)
- Source裏需要被拷貝的字段需要有getter,Target的字段,需要有setter,第三個條件
- Source裏需要被拷貝的字段的getter,和Target裏的setter,都只能是public的(protected/private/不寫即default通通不行),第四個條件
- Source裏的字段,不能是static的(含static final),第五條件
- Target裏的字段,不能是final的(含static final),第六條件
注意:
- BeanUtils是淺拷貝,不是深拷貝,只是把引用弄過去
例如:Source中有A類的字段field,Target中也有A類的字段field,則Source中的field和Target中的field將會是相同的對象(哈希碼相同)
如果Source中有非普通字段,謹慎用BeanUtils,最好將內嵌的轉換後再set到外層,不然很多坑!!!
2. Source裏的父類的字段,也能被拷貝
舉例
注意,無特別表示都表示Source有getter,Target有getter,且都是public的
Source | Target | 是否可以拷貝 |
---|---|---|
字段名:age | 字段名:age2 | 否 |
字段類型:int | 字段類型:int | 是 |
字段類型:int | 字段類型:Integer | 是 |
字段類型:Integer | 字段類型:int | 是 |
字段類型:int | 字段類型:Long | 否 |
字段類型:int | 字段類型:long | 否 |
字段類型:Long | 字段類型:int | 否 |
字段類型:boolean | 字段類型:boolean | 是 |
字段類型:boolean | 字段類型:Boolean | 是 |
字段類型:Boolean | 字段類型:boolean | 是 |
字段類型:Dog(繼承Animal) | 字段類型:Animal | 否 |
字段類型:List<A> |
字段類型:List<B> |
是。雖然有泛型,但能賦值過去,但存在嚴重隱患,見附錄 |
字段類型:Boolean | 字段類型:Boolean | 是 |
Source繼承Super1,Super1裏有name字段 | Target繼承Super2,Super2裏也有name字段 | 是 |
Source繼承Super1,Super1裏有name字段 | Target裏有name字段(不繼承) | 是 |
Source有name字段(不繼承) | Target繼承Super2,Super2裏有name字段 | 是 |
字段修飾:static,如static Integer age |
任意修飾符:如static/static final/普通類型/final | 否。(不拋異常,但讀取不了Source的static字段) |
字段修飾:普通字段 | 任意修飾符:如static/普通類型(除final/static final) | 是(但不能set到Target是final的) |
字段修飾:final | 任意修飾符:如static/普通類型(除final/static final) | 是(但不能set到Target是final的) |
- Source中的serialVersionUID(就是Serializable要求的那個字段),會出現什麼情況? 會覆蓋Target中的嗎? (好像不會吧,這個final的)(不會轉過去,因爲是static的)
附錄
下面演示了一個巨大的坑
public class Test_A_Fucking_Problem {
public static void main(String[] args) {
Source source = new Source();
Pet p1 = new Pet();
p1.setName("dog");
Pet p2 = new Pet();
p2.setName("cat");
List<Pet> petList = new ArrayList<>();
petList.add(p1);
petList.add(p2);
source.setPetList(petList);
Target target = new Target();
BeanUtils.copyProperties(source, target);
System.out.println("target:" + target + ",target.petList.hashCode:"
+ (target.getPetList() == null ? "未被賦值": target.getPetList().hashCode()) + ",petList.hashCode:" + petList.hashCode());
// ★★需要非常注意的點,Source是List<A> 但是Target是List<B>,BeanUtils是可以把值賦值到target的,因爲都是List類型,它不會管泛型不同的
// 但是,賦值過去,實際上是把對象的引用引過去,也就是 `List<B> petList` 字段其實是被賦值了 `List<A> petList` 的實例,在下面運行的過程就出現類轉換異常
// get(0) 還不會出現異常,但是get(0)後再getName,則JVM需要弄清楚這個get(0)得到是什麼類型,當發現類型跟聲明的泛型類型不一樣的時候,就拋出了運行時異常(ClassCast...)
System.out.println(target.getPetList().get(0));
System.out.println(target.getPetList().get(0).getName());
}
}
public class Source {
private List<Pet> petList;
public List<Pet> getPetList() {
return petList;
}
public void setPetList(List<Pet> petList) {
this.petList = petList;
}
}
public class Target {
private List<Pet2> petList;
public List<Pet2> getPetList() {
return petList;
}
public void setPetList(List<Pet2> petList) {
this.petList = petList;
}
}
public class Pet {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
public class Pet2 {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}