【小家Spring】聊聊Spring中的數據綁定 --- BeanWrapper以及內省Introspector和PropertyDescriptor

每篇一句

千古以來要飯的沒有要早飯的,知道爲何嗎?java

相關閱讀

【小家Spring】聊聊Spring中的數據轉換:Converter、ConversionService、TypeConverter、PropertyEditor【小家Spring】聊聊Spring中的數據綁定 --- 屬性訪問器PropertyAccessor和實現類DirectFieldAccessor的使用web

對Spring感興趣可掃碼加入wx羣: Java高工、架構師3羣(文末有二維碼)

前言

這篇文章須要依賴於對屬性訪問器PropertyAccessor的理解,也就是上篇文章的內容:【小家Spring】聊聊Spring中的數據綁定 --- 屬性訪問器PropertyAccessor和實現類DirectFieldAccessor的使用spring

若是說上篇文章所說的PropertyAccessor你沒有接觸過和聽過,那麼本文即將要說的重點:BeanWrapper你應該多少有所耳聞吧~BeanWrapper能夠簡單的把它理解爲:一個方便開發人員使用字符串來對Java Bean的屬性執行get、set操做的工具。關於它的數據轉換使用了以下兩種機制:數組

  1. PropertyEditor:隸屬於Java Bean規範PropertyEditor只提供了String <-> Object 的轉換。
  2. ConversionService:Spring自3.0以後提供的替代PropertyEditor的機制(BeanWrapper在Spring的第一個版本就存在了~)

    按照Spring官方文檔的說法,當容器內沒有註冊ConversionService的時候,會退回使用PropertyEditor機制。言外之意:首選方案是ConversionService緩存

    其實瞭解的夥伴應該知道,這不是BeanWrapper的內容,而是父接口PropertyAccessor的內容~架構

BeanWrapper

官方解釋:Spring低級JavaBeans基礎設施的中央接口。一般來講並不直接使用BeanWrapper,而是藉助BeanFactory或者DataBinder來一塊兒使用~app

//@since 13 April 2001  很清晰的看到,它也是個`PropertyAccessor`屬性訪問器
public interface BeanWrapper extends ConfigurablePropertyAccessor {

    // @since 4.1
    void setAutoGrowCollectionLimit(int autoGrowCollectionLimit);
    int getAutoGrowCollectionLimit();


    Object getWrappedInstance();
    Class<?> getWrappedClass();

    // 獲取屬性們的PropertyDescriptor  獲取屬性們
    PropertyDescriptor[] getPropertyDescriptors();
    // 獲取具體某一個屬性~
    PropertyDescriptor getPropertyDescriptor(String propertyName) throws InvalidPropertyException;
}複製代碼

BeanWrapper至關於一個代理器,Spring委託BeanWrapper完成Bean屬性的填充工做。關於此接口的實現類,簡單的說它只有惟一實現類:BeanWrapperImpl框架

BeanWrapperImpl

它做爲BeanWrapper接口的默認實現,它足以知足全部的典型應用場景,它會緩存Bean的內省結果而提升效率。編輯器

在Spring2.5以前,此實現類是非public的,但在2.5以後給public了而且還提供了工廠:PropertyAccessorFactory幫助第三方框架能快速獲取到一個實例~ide

public class BeanWrapperImpl extends AbstractNestablePropertyAccessor implements BeanWrapper {

    // 緩存內省結果~
    @Nullable
    private CachedIntrospectionResults cachedIntrospectionResults;
    // The security context used for invoking the property methods.
    @Nullable
    private AccessControlContext acc;

    // 構造方法都是沿用父類的~
    public BeanWrapperImpl() {
        this(true);
    }
    ... 
    private BeanWrapperImpl(Object object, String nestedPath, BeanWrapperImpl parent) {
        super(object, nestedPath, parent);
        setSecurityContext(parent.acc);
    }


    // @since 4.3  設置目標對象~~~
    public void setBeanInstance(Object object) {
        this.wrappedObject = object;
        this.rootObject = object;
        this.typeConverterDelegate = new TypeConverterDelegate(this, this.wrappedObject);
        // 設置內省的clazz
        setIntrospectionClass(object.getClass());
    }

    // 複寫父類的方法  增長內省邏輯
    @Override
    public void setWrappedInstance(Object object, @Nullable String nestedPath, @Nullable Object rootObject) {
        super.setWrappedInstance(object, nestedPath, rootObject);
        setIntrospectionClass(getWrappedClass());
    }

    // 若是cachedIntrospectionResults它持有的BeanClass並非傳入的clazz 那就清空緩存 從新來~~~
    protected void setIntrospectionClass(Class<?> clazz) {
        if (this.cachedIntrospectionResults != null && this.cachedIntrospectionResults.getBeanClass() != clazz) {
            this.cachedIntrospectionResults = null;
        }
    }
    private CachedIntrospectionResults getCachedIntrospectionResults() {
        if (this.cachedIntrospectionResults == null) {
            // forClass此方法:生成此clazz的類型結果,而且緩存了起來~~
            this.cachedIntrospectionResults = CachedIntrospectionResults.forClass(getWrappedClass());
        }
        return this.cachedIntrospectionResults;
    }
    ...

    // 獲取到此屬性的處理器。此處是個BeanPropertyHandler 內部類~
    @Override
    @Nullable
    protected BeanPropertyHandler getLocalPropertyHandler(String propertyName) {
        PropertyDescriptor pd = getCachedIntrospectionResults().getPropertyDescriptor(propertyName);
        return (pd != null ? new BeanPropertyHandler(pd) : null);
    }
    @Override
    protected BeanWrapperImpl newNestedPropertyAccessor(Object object, String nestedPath) {
        return new BeanWrapperImpl(object, nestedPath, this);
    }
    @Override
    public PropertyDescriptor[] getPropertyDescriptors() {
        return getCachedIntrospectionResults().getPropertyDescriptors();
    }

    // 獲取具體某一個屬性的PropertyDescriptor 
    @Override
    public PropertyDescriptor getPropertyDescriptor(String propertyName) throws InvalidPropertyException {
        BeanWrapperImpl nestedBw = (BeanWrapperImpl) getPropertyAccessorForPropertyPath(propertyName);
        String finalPath = getFinalPath(nestedBw, propertyName);
        PropertyDescriptor pd = nestedBw.getCachedIntrospectionResults().getPropertyDescriptor(finalPath);
        if (pd == null) {
            throw new InvalidPropertyException(getRootClass(), getNestedPath() + propertyName, "No property '" + propertyName + "' found");
        }
        return pd;
    }
    ...

    // 此處理器處理的是PropertyDescriptor 
    private class BeanPropertyHandler extends PropertyHandler {
        private final PropertyDescriptor pd;

        // 是否可讀、可寫  都是由PropertyDescriptor 去決定了~
        // java.beans.PropertyDescriptor~~
        public BeanPropertyHandler(PropertyDescriptor pd) {
            super(pd.getPropertyType(), pd.getReadMethod() != null, pd.getWriteMethod() != null);
            this.pd = pd;
        }
        ...
        @Override
        @Nullable
        public Object getValue() throws Exception {
            ...
            ReflectionUtils.makeAccessible(readMethod);
            return readMethod.invoke(getWrappedInstance(), (Object[]) null);
        }
        ...
    }
}複製代碼

從繼承體系上,首先咱們應該能看出來BeanWrapperImpl的三重身份:

  1. Bean包裹器
  2. 屬性訪問器(PropertyAccessor)
  3. 屬性編輯器註冊表(PropertyEditorRegistry)

從源碼中繼續分析還能再得出以下兩個結論:

  1. 它給屬性賦值調用的是Method方法,如readMethod.invokewriteMethod.invoke
  2. 它對Bean的操做,大都委託給CachedIntrospectionResults去完成~

所以若想了解它,必然主要是要先了解java.beans.PropertyDescriptororg.springframework.beans.CachedIntrospectionResults,首當其衝的天然還有Java內省

Java內省Introspector

首先能夠先了解下JavaBean的概念:一種特殊的類,主要用於傳遞數據信息。這種類中的方法主要用於訪問私有的字段,且方法名符合某種命名規則。若是在兩個模塊之間傳遞信息,能夠將信息封裝進JavaBean中,這種對象稱爲「值對象」(Value Object),或「VO」。

所以JavaBean都有以下幾個特徵:

  1. 屬性都是私有的;
  2. 有無參的public構造方法;
  3. 對私有屬性根據須要提供公有的getXxx方法以及setXxx方法;
  4. getters必須有返回值沒有方法參數;setter值沒有返回值,有方法參數;

符合這些特徵的類,被稱爲JavaBean;JDK中提供了一套API用來訪問某個屬性的getter/setter方法,這些API存放在java.beans中,這就是內省(Introspector)

==內省和反射的區別==

反射:Java反射機制是在運行中,對任意一個類,可以獲取獲得這個類的全部屬性和方法;它針對的是任意類內省(Introspector):是Java語言對JavaBean類屬性、事件的處理方法

  1. 反射能夠操做各類類的屬性,而內省只是經過反射來操做JavaBean的屬性
  2. 內省設置屬性值確定會調用seter方法,反射能夠不用(反射可直接操做屬性Field)
  3. 反射就像照鏡子,而後能看到.class的全部,是客觀的事實。內省更像主觀的判斷:好比看到getName()內省就會認爲這個類中有name字段,但事實上並不必定會有name;經過內省能夠獲取bean的getter/setter

既然反射比內省比內省強大這麼多,那內省用在何時場景呢?下面給出一個示例來講明它的用武之地:

// 就這樣簡單幾步,就完成了表單到User對象的封裝~
    public void insertUser(HttpServletRequest request) throws Exception {
        User user = new User();

        // 遍歷:根據字段名去拿值便可(此處省略判空、類型轉換等細節,不在本文討論範圍)
        PropertyDescriptor[] pds = Introspector.getBeanInfo(User.class).getPropertyDescriptors();
        for (PropertyDescriptor pd : pds) {
            pd.getWriteMethod().invoke(user, request.getParameter(pd.getName()));
        }
    }複製代碼

經過內省能夠很輕鬆的將form表單的內容填充進對象裏面,比反射輕鬆省力多了。其實像MyBatis這種框架,底層都用到了Java的內省機制。

內省的API主要有Introspector、BeanInfo、PropertyDescriptor等,下面就以他三爲例來操做一個JavaBean:

@Getter
@Setter
@ToString
public class Child {

    private String name;
    private Integer age;
    
}複製代碼
使用Introspector + BeanInfo
public static void main(String[] args) throws IntrospectionException {
        BeanInfo beanInfo = Introspector.getBeanInfo(Child.class);

        BeanDescriptor beanDescriptor = beanInfo.getBeanDescriptor();
        MethodDescriptor[] methodDescriptors = beanInfo.getMethodDescriptors();
        PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();

        // 打印
        System.out.println(beanDescriptor);
        System.out.println("------------------------------");
        Arrays.stream(methodDescriptors).forEach(x -> System.out.println(x));
        System.out.println("------------------------------");
        Arrays.stream(propertyDescriptors).forEach(x -> System.out.println(x));
        System.out.println("------------------------------");
    }複製代碼

輸入內容以下:

java.beans.BeanDescriptor[name=Child; beanClass=class com.fsx.bean.Child]
------------------------------
java.beans.MethodDescriptor[name=getClass; method=public final native java.lang.Class java.lang.Object.getClass()]
java.beans.MethodDescriptor[name=getName; method=public java.lang.String com.fsx.bean.Child.getName()]
java.beans.MethodDescriptor[name=setAge; method=public void com.fsx.bean.Child.setAge(java.lang.Integer)]
java.beans.MethodDescriptor[name=setName; method=public void com.fsx.bean.Child.setName(java.lang.String)]
java.beans.MethodDescriptor[name=getAge; method=public java.lang.Integer com.fsx.bean.Child.getAge()]
java.beans.MethodDescriptor[name=wait; method=public final void java.lang.Object.wait() throws java.lang.InterruptedException]
java.beans.MethodDescriptor[name=notifyAll; method=public final native void java.lang.Object.notifyAll()]
java.beans.MethodDescriptor[name=notify; method=public final native void java.lang.Object.notify()]
java.beans.MethodDescriptor[name=wait; method=public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException]
java.beans.MethodDescriptor[name=hashCode; method=public native int java.lang.Object.hashCode()]
java.beans.MethodDescriptor[name=wait; method=public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException]
java.beans.MethodDescriptor[name=equals; method=public boolean java.lang.Object.equals(java.lang.Object)]
java.beans.MethodDescriptor[name=toString; method=public java.lang.String com.fsx.bean.Child.toString()]
------------------------------
java.beans.PropertyDescriptor[name=age; propertyType=class java.lang.Integer; readMethod=public java.lang.Integer com.fsx.bean.Child.getAge(); writeMethod=public void com.fsx.bean.Child.setAge(java.lang.Integer)]
java.beans.PropertyDescriptor[name=class; propertyType=class java.lang.Class; readMethod=public final native java.lang.Class java.lang.Object.getClass()]
java.beans.PropertyDescriptor[name=name; propertyType=class java.lang.String; readMethod=public java.lang.String com.fsx.bean.Child.getName(); writeMethod=public void com.fsx.bean.Child.setName(java.lang.String)]
------------------------------複製代碼

能夠看到getMethodDescriptors()它把父類的MethodDescriptor也拿出來了。而PropertyDescriptor中比較特殊的是由於有getClass()方法,所以class也算是一個PropertyDescriptor,可是它沒有writeMethod哦~

關於BeanInfo,Spring在3.1提供了一個類ExtendedBeanInfo繼承自它實現了功能擴展,而且提供了BeanInfoFactory來專門生產它~~~(實現類爲:`ExtendedBeanInfoFactory`)

可是若是隻想拿某一個屬性的話,使用Introspector就不是那麼方便了,下面介紹更爲經常使用的PropertyDescriptor來處理某一個屬性~

PropertyDescriptor 屬性描述器

屬性描述符描述了Java bean經過一對訪問器方法導出的一個屬性。上面的示例此處用PropertyDescriptor試試:

public static void main(String[] args) throws IntrospectionException {
        PropertyDescriptor age = new PropertyDescriptor("age", Child.class);
        System.out.println(age.getPropertyType()); //class java.lang.Integer
        System.out.println(age.getDisplayName()); //age


        // 最重要的兩個方法~~~
        System.out.println(age.getReadMethod()); //public java.lang.Integer com.fsx.bean.Child.getAge()
        System.out.println(age.getWriteMethod()); //public void com.fsx.bean.Child.setAge(java.lang.Integer)
    }複製代碼

能夠看到它能夠實現更加細粒度的控制。將PropertyDescriptor類的一些主要方法描述以下:

  1. getPropertyType(),得到屬性的Class對象;
  2. getReadMethod(),得到用於讀取屬性值的方法;
  3. getWriteMethod(),得到用於寫入屬性值的方法;
  4. setReadMethod(Method readMethod),設置用於讀取屬性值的方法;
  5. setWriteMethod(Method writeMethod),設置用於寫入屬性值的方法。

---

CachedIntrospectionResults

Spring若是須要依賴注入那麼就必須依靠Java內省這個特性了,說到Spring IOC與JDK內省的結合那麼就不得不說一下Spring中的CachedIntrospectionResults這個類了。它是Spring提供的專門用於緩存JavaBean的PropertyDescriptor描述信息的類,不能被應用代碼直接使用。

它的緩存信息是被靜態存儲起來的(應用級別),所以對於同一個類型的被操做的JavaBean並不會都建立一個新的CachedIntrospectionResults,所以,這個類使用了工廠模式,使用私有構造器和一個靜態的forClass工廠方法來獲取實例。

public final class CachedIntrospectionResults {
    
    // 它能夠經過在spring.properties裏設置這個屬性,來關閉內省的緩存~~~
    public static final String IGNORE_BEANINFO_PROPERTY_NAME = "spring.beaninfo.ignore";
    private static final boolean shouldIntrospectorIgnoreBeaninfoClasses = SpringProperties.getFlag(IGNORE_BEANINFO_PROPERTY_NAME);

    // 此處使用了SpringFactoriesLoader這個SPI來加載BeanInfoFactory,惟一實現類是ExtendedBeanInfoFactory
    /** Stores the BeanInfoFactory instances. */
    private static List<BeanInfoFactory> beanInfoFactories = SpringFactoriesLoader.loadFactories(
            BeanInfoFactory.class, CachedIntrospectionResults.class.getClassLoader());

    static final Set<ClassLoader> acceptedClassLoaders = Collections.newSetFromMap(new ConcurrentHashMap<>(16));
    static final ConcurrentMap<Class<?>, CachedIntrospectionResults> strongClassCache = new ConcurrentHashMap<>(64);
    static final ConcurrentMap<Class<?>, CachedIntrospectionResults> softClassCache = new ConcurrentReferenceHashMap<>(64);

    // 被包裹類的BeanInfo~~~也就是目標類
    private final BeanInfo beanInfo;
    // 它緩存了被包裹類的全部屬性的屬性描述器PropertyDescriptor。
    private final Map<String, PropertyDescriptor> propertyDescriptorCache;



    ... // 其它的都是靜態方法
    // 只有它會返回一個實例,此類是單例的設計~  它保證了每一個beanClass都有一個CachedIntrospectionResults 對象,而後被緩存起來~
    static CachedIntrospectionResults forClass(Class<?> beanClass) throws BeansException { ... }
}複製代碼

本處理類的核心內容是Java內省getBeanInfo()以及PropertyDescriptor~注意:爲了使此內省緩存生效,有個前提條件請保證了:

  • 確保將Spring框架的Jar包和你的應用類使用的是同一個ClassLoader加載的,這樣在任何狀況下會容許隨着應用的生命週期來清楚緩存。

所以對於web應用來講,Spring建議給web容器註冊一個IntrospectorCleanupListener監聽器來防止多ClassLoader佈局,這樣也能夠有效的利用caching從而提升效率~

監聽器的配置形如這樣(此處以web.xml裏配置爲例):

<listener>
    <listener-class>org.springframework.web.util.IntrospectorCleanupListener</listener-class>
</listener>複製代碼

說明:請保證此監聽器配置在第一個位置,比ContextLoaderListener還靠前~ 此監聽器能有效的防止內存泄漏問題~~~(由於內省的緩存是應用級別的全局緩存,很容易形成泄漏的~)

其實流行框架好比struts, Quartz等在使用JDK的內省時,存在沒有釋的內存泄漏問題~
---

DirectFieldAccessFallbackBeanWrapper

說完了BeanWrapperImpl,能夠看看它的子類DirectFieldAccessFallbackBeanWrapper,他就像BeanWrapperImplDirectFieldAccessor的結合體。它先用BeanWrapperImpl.getPropertyValue(),若拋出異常了(畢竟內省不是十分靠譜,哈哈)再用DirectFieldAccessor~~~此子類在`JedisClusterConnection`有被使用到過,比較簡單沒啥太多好說的~

PropertyAccessorFactory

Spring2.5後提供的快速獲取PropertyAccessor兩個重要實現類的工廠。

public final class PropertyAccessorFactory {
    private PropertyAccessorFactory() {
    }
    // 生產一個BeanWrapperImpl(最爲經常使用)
    public static BeanWrapper forBeanPropertyAccess(Object target) {
        return new BeanWrapperImpl(target);
    }
    // 生產一個DirectFieldAccessor
    public static ConfigurablePropertyAccessor forDirectFieldAccess(Object target) {
        return new DirectFieldAccessor(target);
    }

}複製代碼

BeanWrapper使用Demo

說了這麼多,是時候實戰一把了~

// 省略Apple類和Size類,有須要的請參照上篇文章(加上@Getter、@Setter便可

    public static void main(String[] args) {
        Apple apple = new Apple();

        BeanWrapper beanWrapper = PropertyAccessorFactory.forBeanPropertyAccess(apple);

        // ================看成一個普通的PropertyAccessor來使用  默認狀況下字段也都必須有初始值才行~===================
        // 設置普通屬性
        beanWrapper.setPropertyValue("color", "紅色"); //請保證對應字段有set方法才行,不然拋錯:Does the parameter type of the setter match the return type of the getter?
        // 設置嵌套屬性(注意:此處可以正常work是由於有= new Size(),
        // 不然報錯:Value of nested property 'size' is null 下同~)
        beanWrapper.setPropertyValue("size.height", 10);

        // 設置集合/數組屬性
        beanWrapper.setPropertyValue("arrStr[0]", "arrStr");
        beanWrapper.setPropertyValue("arrStr[1]", "arrStr1"); // 注意:雖然初始化時初始化過數組了,可是仍以此處的爲準
        // =========打印輸出
        System.out.println(apple); //Apple(color=紅色, size=Size(height=10, width=null), arrStr=[arrStr, arrStr1], listStr=[], map={}, listList=[[]], listMap=[{}])


        // 看成BeanWrapper使用
        PropertyDescriptor[] propertyDescriptors = beanWrapper.getPropertyDescriptors();
        PropertyDescriptor color = beanWrapper.getPropertyDescriptor("color");
        System.out.println(propertyDescriptors.length); // 8
        System.out.println(color); //org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=color]

        System.out.println(beanWrapper.getWrappedClass()); //class com.fsx.bean.Apple
        System.out.println(beanWrapper.getWrappedInstance()); //Apple(color=紅色, size=Size(height=10...
    }複製代碼

上面代碼可以清晰的表示了經過BeanWrapper來操做JavaBean仍是很是之簡便的。

最後,上一張比較醜的結構圖,畫一畫屬性編輯器、類型轉換器、屬性解析器、屬性訪問器大體的一個關係(此圖不喜勿碰):在這裏插入圖片描述

總結

BeanWrapper接口,做爲Spring內部的一個核心接口,正如其名,它是bean的包裹類,即在內部中將會保存該bean的實例,提供其它一些擴展功能。

Spring對Bean的屬性存取都是經過BeanWrapperImpl實現的,BeanWrapperImplBean是一對一的關係,BeanWrapperImpl經過屬性的讀方法寫方法來存取Bean屬性的。爲了更加深入的瞭解BeanWrapper,下篇文章會深刻分析Spring BeanFactory對它的應用~

知識交流

==The last:若是以爲本文對你有幫助,不妨點個讚唄。固然分享到你的朋友圈讓更多小夥伴看到也是被做者本人許可的~==

**若對技術內容感興趣能夠加入wx羣交流:`Java高工、架構師3羣`。若羣二維碼失效,請加wx號:fsx641385712(或者掃描下方wx二維碼)。而且備註:"java入羣" 字樣,會手動邀請入羣**在這裏插入圖片描述

相關文章
相關標籤/搜索