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的內省機制。

BeanInfo示例

JavaBean內省工具Introspector

Java bean的工具包中提供了java內省工具Introspector,該工具可以通過以下方法獲取Java bean 的內省結果BeanInfo(後文詳細介紹),獲取BeanInfo的流程如下圖所示

// 在Object類時候停止檢索,可以選擇在任意一個父類停止
BeanInfo beanInfo = Introspector.getBeanInfo(JavaBeanDemo.class,Object.class);

Introspector

JavaBean內省結果BeanInfo

通過java的內省工具Introspector的getBeanInfo方法,我們可以獲取一個JavaBean的內省BeanInfo,獲取到的BeanInfo包含以下屬性:

  • Bean的類相關資訊
  • Bean的事件資訊
  • Bean的屬性資訊
  • Bean的方法資訊
  • 額外屬性資訊
  • Component的圖標

BeanInfo

內省結果BeanInfo的類型

BeanInfo只是一個內省結果的介面,Java中對該介面的實現有以下三種:

  1. ApplicationBeanInfo:Apple desktop相關的JavaBean內省結果
  2. ComponentBeanInfo:Java Awt組件的內省結果,如按鈕等
  3. GenericBeanInfo:通用的內省結果,JEE開發中的內省結果都為該類型

此外,Spring自定義了一個內省結果類型,叫ExtendedBeanInfo,主要用於識別返回值不為空的Setter方法。

BeanInfo的擴展

Spring的BeanUtils.copyProperties

BeanUtils.copyProperties用戶在兩個對象之間進行屬性的複製,底層基於JavaBean的內省機制,通過內省得到拷貝源對象和目的對象屬性的讀方法和寫方法,然後調用對應的方法進行屬性的複製。以下為BeanUtils.copyProperties的流程

BeanUtils內省流程

BeanUtils對JavaBean內省的一些機制進行優化,到這裡,大家有沒有發現Java內省的一些缺點呢?

BeanUtils並發問題優化

Java內省的結果會快取在ThreadGroupContext中,並且通過synchonrized關鍵字對快取加鎖(下圖中的紅框部分),導致同一個執行緒組中的執行緒無法並行內省。

BeanUtils並發問題優化

Spring的BeanUtils在Java內省之上又添加了一層快取,這層快取使用ConcurrentHashMap實現,從而提高了內省的效率。

BeanUtils Setter屬性識別優化

在Java默認的內省過程中,setter方法的返回值必須是null,如果不是null的話,無法識別為有效的JavaBean屬性(下圖中的紅色部分),Spring 自定義了一個BeanInfo ExtendedBeanInfo解決了這個問題。

Setter屬性識別優化

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

qrcode_for_gh_83670e17bbd7_344-2021-09-04-10-55-16

本文最先發布至微信公眾號,版權所有,禁止轉載!

Tags: