SpringMVC源碼分析4、從Java內省機制到BeanWrapper原理

Java內省機制

描述

之因此在SpringMVC源碼分析中穿插這麼一篇關於Java知識的文章, 是由於咱們即將分析的HandlerAdapter底層對數據
的綁定就是基於Java內省機制的, 在以前Spring源碼分析中, 筆者也或多或少提到過, 可是當時沒有細講, 到了
SpringMVC中就不的不提了, 否則有些代碼可能會讓讀者感到迷惑

在這裏筆者先以我本身的理解來講下什麼是Java的內省機制, Java內省機制是對反射的一種封裝, 是Java提供給開發
者對一個對象屬性的查看和操做、方法的操做以及對象的描述等, 或許這樣說比較抽象, 以後咱們會舉一些例子來講明

固然, 這個機制在對於屬性操做的時候是有必定限制的, 這個咱們留個懸念, 文章以後會特別對這一塊說明

Java的內省機制, 不可避免的會涉及到幾個類: Introspector、PropertyDescriptor、PropertyEditor以及
BeanInfo, 接下來咱們詳細描述下這幾個類, 並用一些例子來演示, 這樣你們就可以更加清晰的理解Java的內省機制
了
複製代碼

PropertyDescriptor屬性描述器

先以一個例子來入門吧:

public class User {
    private String name;

    private String aName;

    getter / setter / toString
}

public static void main (String[] args) throws Exception {
    User user = new User();
    System.out.println( user );
    PropertyDescriptor propertyDescriptor = new PropertyDescriptor( "name", User.class );
    Method readMethod = propertyDescriptor.getReadMethod();
    System.out.println( readMethod.invoke( user ) );
    Method writeMethod = propertyDescriptor.getWriteMethod();
    writeMethod.invoke( user, "hello" );
    System.out.println( user );
}

輸出結果:
    User{name='null', aName='null'}
    null
    User{name='hello', aName='null'}

分析:
    能夠看到, 咱們先建立了一個對象, 而後輸出的結果中裏面的屬性都是null, 這個就不用解釋了吧..........
    
    而後咱們建立了一個PropertyDescriptor屬性描述器, 傳入了屬性名稱和所在的類, 這時候你們應該就可能會
    想到, 這個PropertyDescriptor中應該是有兩個屬性, 可能分別叫targetClass, propertyName這樣的, 從而
    保存了這兩個傳入的值, 好的, 接着往下看......

    getReadMethod獲取讀方法, 什麼是讀方法呢?就是getXXXX, 那咱們描述的屬性是name, 獲取的天然就是getName
    了, 返回值是一個Method對象, 經過反射調用, 傳入user對象, 就能獲取到該對象中的name屬性

    getWriteMethod獲取寫方法, 什麼是寫方法呢?就是setXXXX, 那咱們描述的屬性是name, 獲取的天然就是setName
    了, 返回值是一個Method對象, 經過反射調用, 傳入user對象和指望設置的值, 再次輸出user對象的時候, 會發
    現name已經被設置好值了

    上面幾步操做通過描述, 你們應該有點感受了是吧.....沒錯, PropertyDescriptor屬性描述器, 就是對屬性反
    射的一種封裝, 方便咱們直接操做屬性, 當咱們利用構造方法new的時候, 內部就會幫咱們找出這個屬性的get和
    set方法, 分別做爲這個屬性的讀方法和寫方法, 咱們經過PropertyDescriptor對屬性的操做, 其實就是利用反
    射對其get和set方法的操做而已, 可是其內部實現仍是有點意思的, 利用軟引用和弱引用來保存方法、以及Class
    對象的引用, 這個軟引用和弱引用, 筆者以後也會把以前寫的關於這個Java四大引用類型的文章也上傳到掘金, 
    你們要是以前沒了解過四大引用類型的話, 能夠理解爲這是爲了防止內存泄露的一種操做就行了, 或者直接理解爲
    就是一個直接引用就能夠了........

接下來再來看一個例子, 咱們重用以前的User類:

public static void main (String[] args) throws Exception{
    PropertyDescriptor propertyDescriptor = new PropertyDescriptor( "aName", User.class );
    Method readMethod = propertyDescriptor.getReadMethod();
    Method writeMethod = propertyDescriptor.getWriteMethod();
}

分析:
    能夠看到, 此時咱們要建立的屬性描述器是User這個類中的aName屬性, 這個aName屬性的get和set方法是這樣的
        public String getaName() {
            return aName;
        }

        public void setaName(String aName) {
            this.aName = aName;
        }

    這個是idea自動生成的, 或者編輯器自動生成的get/set就是這樣的, 可是咱們會發現, 當執行上述的main方法
    的時候, 居然報錯了, 報錯信息: java.beans.IntrospectionException: Method not found: isAName

    由於Java內省機制是有必定的規範的, 查找一個屬性的get/set方法, 須要將該屬性的第一個字母變成大寫, 而後
    前面拼接上get/set前綴, 因爲咱們編輯器生成的get/set方法在屬性前兩個字母一大一小的狀況下, 不會改變其
    set和get方法的前兩個字母, 因此致使了報錯, 這一點須要注意, 筆者以前但是在項目中吃過一次虧的!!!由於
    SpringMVC的數據綁定就是基於Java的內省機制來完成的, 那麼當筆者的一個屬性開頭兩個字母是一大一小的時候
    死活綁定不到對應的數據.......然而在內部處理的時候, 異常已經被吞掉了....
複製代碼

PropertyEditor屬性編輯器

PropertyEditor也是Java提供的一種對屬性的擴展了, 用於類型的轉換, 好比咱們設置屬性的時候, 指望將String類
型轉爲其餘類型後再設置到對象中, 就須要利用到這個屬性編輯器, Java中PropertyEditor接口有一個直接子類
PropertyEditorSupport, 該類基本是這樣定義的(僞代碼):
public class PropertyEditorSupport implements PropertyEditor {
    private Object value;

    public Object getValue() {
        return value;
    }

    public void setValue(Object value) {
        this.value = value;
    }

    public void setAsText(String text) throws java.lang.IllegalArgumentException {
        if (value instanceof String) {
            setValue(text);
            return;
        }
        throw new java.lang.IllegalArgumentException(text);
    }

    public String getAsText() {
        return (this.value != null)
                ? this.value.toString()
                : null;
    }
}


分析:
    上面這個代碼是PropertyEditorSupport的一小部分, 能夠看到其實就是一個簡單的類而已, 裏面有一個Object
    類型的屬性value, 提供了get/set方法, 與此同時, 提供了setAsText和getAsText方法, 一般咱們須要繼承這
    個類來完成擴展, 好比說這個value是一個Date類型, 咱們指望設置的時候提供的是字符串, 那麼就要繼承這個類
    並重寫其setAsText方法, 以下:

    public class DatePropertyEditor extends PropertyEditorSupport {
        @Override
        public String getAsText() {
            Date value = (Date) getValue();
            DateFormat dateFormat = new SimpleDateFormat( "yyyy-MM-dd HH:mm:ss" );
            return dateFormat.format( value );
        }

        @Override
        public void setAsText(String text) throws IllegalArgumentException {
            DateFormat dateFormat = new SimpleDateFormat( "yyyy-MM-dd HH:mm:ss" );
            try {
                Date date = dateFormat.parse( text );
                setValue( date );
            } catch (ParseException e) {}
        }
    }

咱們先來寫個測試類玩玩吧, 這時候先不去分析PropertyEditorSupport中的其餘功能, 先以最簡單的開始:

public static void main(String[] args) throws Exception{
    DatePropertyEditor datePropertyEditor = new DatePropertyEditor();
    datePropertyEditor.setAsText( "2020-03-06 15:33:33" );
    Date value = (Date) datePropertyEditor.getValue();
    System.out.println( value );
}

建立了一個自定義的日期屬性編輯器, 結合上面該類的代碼, 當調用setAsText的時候, 傳入了一個字符串日期, 那麼
就會被解析成一個Date類型, 最後保存到value中, 從而getValue返回的類型就是Date類型, 這個應該很容易理解, 
那麼到這裏, 咱們算是入門了, 簡單的體會了下其功能, 該類中還有一個source屬性和listeners屬性, 這個咱們就
簡單介紹下, source, 一般是咱們須要操做的對象, listeners就是監聽器, 在setValue調用時, 除了直接賦值
this.value = value外, 還會觸發全部的監聽器, 調用監聽器的方法, 監聽器方法中會傳入一個事件對象, 事件對象
中保存了該source, 也就是說, PropertyEditorSupport中有一個Object類型的source屬性, 同時有一個監聽器對象
集合, 這個source屬性能夠在監聽器對象方法被調用的時候獲取到(存在於事件中, 調用監聽器方法會放入一個事件對
象, 構造該事件對象會傳入source), 因爲暫時沒有用到這兩個, 因此先不進行擴展, 沒有應用場景, 擴展也沒多大意
義
複製代碼

BeanInfo

BeanInfo是一個接口, 有一個子類GenericBeanInfo, 一般狀況下建立的是GenericBeanInfo, 其是Introspector
中的一個包訪問權下的類, 咱們先來簡單看看其結構吧:

class GenericBeanInfo extends SimpleBeanInfo {
    private BeanDescriptor beanDescriptor;
    private PropertyDescriptor[] properties;
    private MethodDescriptor[] methods;
}

分析:
    只列舉出了幾個簡單的屬性, 可是夠用了, BeanDescriptor就是持有類Class對象的引用而已, 
    PropertyDescriptor中就是這個類的全部屬性描述器, MethodDescriptor天然就全部的方法描述器了, 跟屬性
    描述器是相似的, 都是爲了方便反射調用的, 那麼BeanInfo的做用就出來了, 就是對一個類全部的屬性、方法等
    反射操做封裝後的集合體, 那麼它如何獲得呢?這就用到了Introspector這個類了, 以下:

    public static void main(String[] args) throws Exception {
        BeanInfo beanInfo = Introspector.getBeanInfo( Customer.class );
        PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
        MethodDescriptor[] methodDescriptors = beanInfo.getMethodDescriptors();
        BeanDescriptor beanDescriptor = beanInfo.getBeanDescriptor();
    }

    那麼到此爲止, 咱們要講解的內省機制的關係就出來了, 經過Introspector獲取一個類的BeanInfo, 經過
    BeanInfo可以獲取屬性描述器、方法描述器、類Class對象, 利用獲取到的屬性描述器, 咱們可以往一個該類實例
    中放入數據
複製代碼

CachedIntrospectionResults

--------------看到下面代碼別慌......請直接看着下面個人文字分析來看代碼---------------
public class CachedIntrospectionResults {
    static final ConcurrentMap<Class<?>, CachedIntrospectionResults> strongClassCache =
        new ConcurrentHashMap<>(64);

    static final ConcurrentMap<Class<?>, CachedIntrospectionResults> softClassCache =
        new ConcurrentReferenceHashMap<>(64);

    private final BeanInfo beanInfo;

	private final Map<String, PropertyDescriptor> propertyDescriptorCache;
    
    static CachedIntrospectionResults forClass(Class<?> beanClass) throws BeansException {
		CachedIntrospectionResults results = strongClassCache.get(beanClass);
		if (results != null) {
			return results;
		}
		results = softClassCache.get(beanClass);
		if (results != null) {
			return results;
		}

		results = new CachedIntrospectionResults(beanClass);
		ConcurrentMap<Class<?>, CachedIntrospectionResults> classCacheToUse;

		if (ClassUtils.isCacheSafe(beanClass, CachedIntrospectionResults.class.getClassLoader()) ||
				isClassLoaderAccepted(beanClass.getClassLoader())) {
			classCacheToUse = strongClassCache;
		}
		else {
			classCacheToUse = softClassCache;
		}

		CachedIntrospectionResults existing = classCacheToUse.putIfAbsent(beanClass, results);
		return (existing != null ? existing : results);
	}
}

分析:
    CachedIntrospectionResults這個類是Spring提供的對類的內省機制使用的工具類, 不一樣於Introspector之處
    在於, 該類提供類內省機制時的數據緩存, 即內省得到的PropertyDescriptor這些數據進行了緩存

    首先咱們來看看最上面的兩個靜態方法, 全局變量保存了兩個Map, key是class對象, value是
    CachedIntrospectionResults對象, 你們應該能夠想到, 這應該是相似於利用Map實現的單例吧, 提供了緩存的
    功能, 以後能夠經過static方法直接訪問

    再來看看其兩個屬性, 一個是BeanInfo, 一個是propertyDescriptorCache, 前者就不用筆者描述了吧, 就是
    內省機制中對一個類的功能的封裝, 前面已經專門對這個類進行說明了, 後者是屬性名到屬性描述器的映射Map,
    這個應該也不用詳細解釋了

    CachedIntrospectionResults類實例, 封裝了一個類經過內省機制得到的BeanInfo和屬性描述器映射, 全局
    的static變量中保存了全部要操做的類的CachedIntrospectionResults類實例緩存, 採用強引用和軟引用是爲
    了防止內存泄露用的

    再來看看forClass, 表示根據Class對象獲取該類的CachedIntrospectionResults類實例, 能夠看到, 先從強
    引用緩存中獲取, 沒拿到則從軟引用中獲取, 這裏你們不熟悉四大引用類型的話, 能夠直接認爲是從Map中根據
    Class對象獲取對應的CachedIntrospectionResults類實例, 若是沒有獲取到, 則建立一個並放到Map中去

小小的總結:
    CachedIntrospectionResults類對象經過全局變量Map提供了對內省機制得到的BeanInfo信息的緩存, 從而能夠
    方便咱們經過static方法獲取對應類的內省信息
複製代碼

BeanWrapper

public class BeanWrapperImpl extends AbstractNestablePropertyAccessor implements BeanWrapper {
	@Nullable
	private CachedIntrospectionResults cachedIntrospectionResults;
}

分析: 
    能夠看到, BeanWrapper實例中內置了一個CachedIntrospectionResults, 以前分析DispathcherSerlvet的
    初始化流程的時候, 小小的說明了下BeanWrapper的做用, 可是沒有分析其怎麼實現屬性的設置的, 這個時候就
    有必要說一下了, 由於其跟咱們SpringMVC數據綁定有點關係

    那既然內部存儲了一個CachedIntrospectionResults實例, 你們應該很容易的想到, 內部就是經過該實例來獲取
    對應的屬性描述器, 而後獲取讀方法和寫方法來設置屬性的嗎?確實如此, 接下來咱們看看setPropertyValue這個
    方法吧, 有不少個重載方法, 咱們以直接經過屬性名和屬性值來設置的這個方法爲例子

public void setPropertyValue(String propertyName, @Nullable Object value) {
    AbstractNestablePropertyAccessor nestedPa=getPropertyAccessorForPropertyPath(propertyName);
    
    PropertyTokenHolder tokens = getPropertyNameTokens(getFinalPath(nestedPa, propertyName));
    nestedPa.setPropertyValue(tokens, new PropertyValue(propertyName, value));
}

簡單解析下, AbstractNestablePropertyAccessor提供了嵌套屬性設置的功能, 好比一個實體類中還有另外一個實體
類, 這種狀況下也是能設置成功的, 不用管這些代碼什麼意思, 下面跟着代碼走, 能夠看到一個豁然開朗的東西...
nestedPa.setPropertyValue中調用了processLocalProperty(tokens, pv), processLocalProperty中獲取了一
個PropertyHandler, 就是經過這個PropertyHandler來完成屬性的設置的, 接下來咱們看看這個PropertyHandler
是什麼

protected BeanPropertyHandler getLocalPropertyHandler(String propertyName) {
    PropertyDescriptor pd 
        = getCachedIntrospectionResults().getPropertyDescriptor(propertyName);
    if (pd != null) {
        return new BeanPropertyHandler(pd);
    }
    return null;
}

是否是以爲豁然開朗, 原來BeanWrapper中, 最終就是經過PropertyDescriptor來完成屬性的設置的!!!!
複製代碼

總結

咱們從Java內省機制進行出發, 引出了PropertyDescriptor、PropertyEditor(類型轉換用)、Introspector、
BeanInfo這四個Java內省機制中的核心類, 同時也捋清楚了內省機制原來就是對反射的封裝, 進而引出了
CachedIntrospectionResults類, 該類是Spring對內省機制中得到的數據的緩存, 進而引出了BeanWrapper的實現
原理, 裏面內置了一個CachedIntrospectionResults對象, 對屬性的操做最終就會變成該對象中的
PropertyDescriptor的操做, 須要說明的是, CachedIntrospectionResults還能提供嵌套的屬性設置, 這個須要
注意, 其實Spring對Java的內省機制的封裝還有不少不少能夠說的, 可是若是僅僅是爲了讀懂SpringMVC的源碼的話,
上面這些內容就夠了, 或許在不久的未來, 筆者會專門寫一個系列來描述Spring對Java內省機制的封裝, 你們能夠
期待期待哈.....
複製代碼
相關文章
相關標籤/搜索