JavaBean內省與BeanInfo
Java的BeanInfo在工作中並不怎麼用到,我也是在學習spring源碼的時候,發現SpringBoot啟動時候會設置一個屬叫”spring.beaninfo.ignore”,網上只能搜索到這個配置的意思是是否跳過java BeanInfo的搜索,沒找到其他資訊,但是BeanInfo又是什麼呢?
JavaBean介紹
維基百科JavaBean的定義:JavaBeans是Java中一種特殊的類,可以將多個對象封裝到一個對象(bean)中。特點是可序列化,提供無參構造器,提供getter方法和setter方法訪問對象的屬性。名稱中的「Bean」是用於Java的可重用軟體組件的慣用叫法。要成為JavaBean類,則必需遵循關於命名、構造器、方法的特定規範。有了這些規範,才能有可以使用、復用、替代和連接JavaBeans的工具。規範如下:
- 有一個public的無參數構造器。
- 屬性可以通過get、set、is(可以替代get,用在布爾型屬性上)方法或遵循特定命名規範的其他方法訪問。
- 可序列化。
以下為一個合法的JavaBean的定義:
public class PersonBean implements java.io.Serializable {
/**
* name 屬性(注意大小寫)
*/
private String name = null;
private boolean deceased = false;
/** 無參構造器(沒有參數) */
public PersonBean() {
}
/**
* name 屬性的Getter方法
*/
public String getName() {
return name;
}
/**
* name 屬性的Setter方法
* @param value
*/
public void setName(final String value) {
name = value;
}
/**
* deceased 屬性的Getter方法
* 布爾型屬性的Getter方法的不同形式(這裡使用了is而非get)
*/
public boolean isDeceased() {
return deceased;
}
/**
* deceased 屬性的Setter方法
* @param value
*/
public void setDeceased(final boolean value) {
deceased = value;
}
}
JavaBean的自省
用一個簡單的SpringMVC用戶登錄的場景來描述一下JavaBean的自省,用戶登錄時候,前端表單傳遞的參數通常是一個如下Json字元串:
{
"username":"xxx",
"password":"xxxx"
}
後端接受表單的地方,通常可以使用一個JavaBean用RequestBody的形式接收參數:
public void login(@RequestBody LoginRequest request){
// Do login
}
其中,LoginRequest類似於如下的格式:
public class LoginRequest {
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
private String username;
private String password;
}
那麼前端的Json如何映射到後端LoginRequest中的對應屬性之上呢?可以看到LoginRequest中的欄位都是private類型,無法直接設置欄位值(反射雖然可以設置,但是並不合適),只能通過Setter方法進行設置,但是程式怎麼知道JavaBean有哪些Setter方法呢?此處就用到了JavaBean的內省機制。
JavaBean內省工具Introspector
Java bean的工具包中提供了java內省工具Introspector,該工具可以通過以下方法獲取Java bean 的內省結果BeanInfo(後文詳細介紹),獲取BeanInfo的流程如下圖所示
// 在Object類時候停止檢索,可以選擇在任意一個父類停止
BeanInfo beanInfo = Introspector.getBeanInfo(JavaBeanDemo.class,Object.class);
JavaBean內省結果BeanInfo
通過java的內省工具Introspector的getBeanInfo方法,我們可以獲取一個JavaBean的內省BeanInfo,獲取到的BeanInfo包含以下屬性:
- Bean的類相關資訊
- Bean的事件資訊
- Bean的屬性資訊
- Bean的方法資訊
- 額外屬性資訊
- Component的圖標
內省結果BeanInfo的類型
BeanInfo只是一個內省結果的介面,Java中對該介面的實現有以下三種:
- ApplicationBeanInfo:Apple desktop相關的JavaBean內省結果
- ComponentBeanInfo:Java Awt組件的內省結果,如按鈕等
- GenericBeanInfo:通用的內省結果,JEE開發中的內省結果都為該類型
此外,Spring自定義了一個內省結果類型,叫ExtendedBeanInfo,主要用於識別返回值不為空的Setter方法。
Spring的BeanUtils.copyProperties
BeanUtils.copyProperties用戶在兩個對象之間進行屬性的複製,底層基於JavaBean的內省機制,通過內省得到拷貝源對象和目的對象屬性的讀方法和寫方法,然後調用對應的方法進行屬性的複製。以下為BeanUtils.copyProperties的流程
BeanUtils對JavaBean內省的一些機制進行優化,到這裡,大家有沒有發現Java內省的一些缺點呢?
BeanUtils並發問題優化
Java內省的結果會快取在ThreadGroupContext中,並且通過synchonrized關鍵字對快取加鎖(下圖中的紅框部分),導致同一個執行緒組中的執行緒無法並行內省。
Spring的BeanUtils在Java內省之上又添加了一層快取,這層快取使用ConcurrentHashMap實現,從而提高了內省的效率。
BeanUtils Setter屬性識別優化
在Java默認的內省過程中,setter方法的返回值必須是null,如果不是null的話,無法識別為有效的JavaBean屬性(下圖中的紅色部分),Spring 自定義了一個BeanInfo ExtendedBeanInfo解決了這個問題。
spring.beaninfo.ignore
回到最初提到的spring.beaninfo.ignore,這個配置用來忽略所有自定義的BeanInfo類的搜索.
BeanUtils 性能測試
複製方法 | 1萬次複製耗時 | 1百萬次複製耗時 | 1億次複製耗時 |
---|---|---|---|
ModelMapper複製 | 262mills | 3875mills | 283177mills |
BeanUtils複製 | 3mills | 369mills | 20347mills |
直接複製 | 約等於0mills | 5mills | 438mills |
可以看出:BeanUtils花費的時間約為直接複製的50倍以上。
public class BeanUtilsPerformanceTest {
public static void main(String[] args){
// 預熱虛擬機
loopBeanUtils(100000);
loopCopyByHand(100000);
// 複製1萬次的情況
System.out.println("\nloop 10000 times:");
loopBeanUtils(10000);
loopCopyByHand(10000);
// 複製1百萬次的情況
System.out.println("\nloop 1000000 times:");
loopBeanUtils(1000000);
loopCopyByHand(1000000);
// 複製1億次的情況
System.out.println("\nloop 100000000 times:");
loopBeanUtils(100000000);
loopCopyByHand(100000000);
}
private static void loopBeanUtils(int loopTimes){
TestBeanDemo source = new TestBeanDemo();
TestBeanDemo target = new TestBeanDemo();
long start = System.currentTimeMillis();
for (int i=0;i<loopTimes;i++){
BeanUtils.copyProperties(source,target);
}
System.out.println("BeanUtils cost times:"+String.valueOf(System.currentTimeMillis()-start));
}
private static void loopCopyByHand(int loopTimes){
TestBeanDemo source = new TestBeanDemo();
TestBeanDemo target = new TestBeanDemo();
long start = System.currentTimeMillis();
for (int i=0;i<loopTimes;i++){
target.setField1(source.getField1());
target.setField2(source.getField2());
target.setField3(source.getField3());
target.setField4(source.getField4());
target.setField5(source.getField5());
}
System.out.println("Copy field one by one times:"+String.valueOf(System.currentTimeMillis()-start));
}
@Data
private static class TestBeanDemo{
private String field1 = UUID.randomUUID().toString();
private String field2 = UUID.randomUUID().toString();
private String field3 = UUID.randomUUID().toString();
private String field4 = UUID.randomUUID().toString();
private String field5 = UUID.randomUUID().toString();
}
}
我是御狐神,歡迎大家關注我的微信公眾號:wzm2zsd
本文最先發布至微信公眾號,版權所有,禁止轉載!