Spring Framework:DataBinder&TypeConvert

示例代碼

參考文檔

涉及問題

  1. 什么是數(shù)據(jù)綁定(Data Binding)凛虽?
  2. DataBinderBeanWrapper 之間的關系?
  3. BeanWrapper 都具備哪些功能 殃姓?
  4. 數(shù)據(jù)綁定過程中的類型轉換(TypeConvert)是怎么實現(xiàn)的瓦阐?

DataBinder

Data binding is useful for letting user input be dynamically bound to the domain model of an application (or whatever objects you use to process user input). Spring provides the aptly named DataBinder to do exactly that. The Validator and the DataBinder make up the validation package, which is primarily used in but not limited to the web layer.

由此可知戳杀,數(shù)據(jù)綁定就是將用戶輸入綁定到領域對象模型的過程。通常是指將用戶提供的一系列屬性與領域對象的 Fields 進行綁定并賦值的過程隔缀。

對于用戶提供的這些屬性猾瘸,spring 提供了一個 PropertyValue 類來進行封裝丢习。其中 name 表示屬性名稱,value 則表示該屬性對應的值揽思,convertedValue 則用來存儲經過轉化后的值(如果有必要的話)钉汗。

public class PropertyValue extends BeanMetadataAttributeAccessor 
    implements Serializable {

    private final String name;

    @Nullable
    private final Object value;

    @Nullable
    private Object convertedValue;
  
  // ...省略部分代碼
}
  • DataBinder

對此,spring 提供了一個 DataBinder 類來提供數(shù)據(jù)綁定功能特恬。它除了提供了將 PropertyValue 綁定到目標對象 target 的功能外,還提供了Validator相關的數(shù)據(jù)校驗功能尝丐。不過此篇我們暫且忽略數(shù)據(jù)校驗部分衡奥,而是先把重心放在數(shù)據(jù)綁定部分矮固。

我們先通過一段簡單的代碼來對 DataBinder 類有一個直觀的了解:

public static void main(String[] args) {
  User user = new User();
  DataBinder dataBinder = new DataBinder(user, "user");

  MutablePropertyValues propertyValues = new MutablePropertyValues();
  propertyValues.addPropertyValue("id", 10);
  propertyValues.addPropertyValue("name", " jerry");
  propertyValues.addPropertyValue("age", 18);

  dataBinder.bind(propertyValues);
  System.out.println(user);
}

這段代碼的輸出結果為:

User{id=10, name='jerry', age=18}

MutablePropertyValues 內置了一個 PropertyValue 的集合档址。我們可以通過 DataBinderbind(propertyValues) 方法來將一個或多個屬性值綁定到目標對象上。
通過對 bind 方法進行 debug 分析绎秒,我們發(fā)現(xiàn)實際上 DataBinder 調用的是一個更底層的類 BeanWrapperImpl 來實現(xiàn)對 Bean 的操作的见芹。

BeanWrapper:一個底層的 Bean 操作類

Spring 官方文檔:

The BeanWrapper is a fundamental concept in the Spring Framework and is used in a lot of places. However, you probably do not need to use the BeanWrapper directly.

如官方文檔所言玄呛,BeanWrapper 作為一個 spring 框架的一個底層基礎工具(通常并不需要直接去使用它)和二,運用于多個地方儿咱。例如:DataBinder 以及 BeanFactory

接下來怠缸,我們便對 BeanWrapper 進行一個詳細的分析:

首先揭北,我們先看一下 BeanWrapper 的默認實現(xiàn)類 BeanWrapperImpl 的類繼承結構圖:

image-20200911232330745

我們重點關注這三個接口搔体,先對它們有一個直觀的了解,然后再逐個分析:

  • BeanWrapper

提供了包裝 Bean 的能力劝术,以及對所封裝 Bean自省能力养晋。(自省是指自我觀察獲取自身內部結構梁钾,自我描述的能力)

  • PropertyAccessor

提供了直觀的操作屬性的能力姆泻。

  • TypeConverter

提供了類型轉換的能力。

那么闭翩,總結一下蛔琅,BeanWrapperImpl 是一個可以包裝一個目標對象罗售,并可以對目標對象進行自省以及屬性賦值的工具類钩述。并且在屬性賦值的過程中支持屬性值的類型轉換牙勘。那么 BeanWrapperImpl 便具備以下能力:

  • 自省能力
  • 屬性存取能力
  • 類型轉化能力

下面呢,將會對這些能力的實現(xiàn)逐一進行更詳細的分析放钦。

Bean Introspection : 自省能力

Bean Introspection 是指 Bean 的自省操禀。

Spring 官方文檔:

The org.springframework.beans package adheres to the JavaBeans standard. A JavaBean is a class with a default no-argument constructor and that follows a naming convention where (for example) a property named bingoMadness would have a setter method setBingoMadness(..) and a getter method getBingoMadness(). For more information about JavaBeans and the specification, see javabeans.

Springorg.springframework.beans 包遵守了 JavaBeans 標準颓屑。一個 JavaBean 呢,是指一個具備默認構造函數(shù)遍搞,并且遵守一定命名規(guī)約的類溪猿。比如纫塌,我們平日里熟知的 gettersetter 方法护戳,則分別對應了相關屬性的讀方法和寫方法媳荒。

java.beans 包下驹饺,提供了一個 Introspector 工具類赏壹,提供了對 JavaBean 的自省能力,通過Introspector.getBeanInfo(Class clazz) 方法昔瞧,可以返回一個自省結果 BeanInfo 對象自晰。通過該對象可以獲取屬性的描述 PropertyDescriptor酬荞,方法的描述 MethodDescriptor 以及Bean 的描述 BeanDescriptor瞧哟。

通過 PropertyDescriptor 則可以獲取對應屬性的類型勤揩,以及相應的讀寫方法陨亡。

BeanWrapperImpl 提供的對所包裝 Bean 的自省能力,實際上也是來源于 java.beans.Introspector辨液。

BeanWrapperImpl 類中箱残,存在這樣一個成員變量:CachedIntrospectionResults被辑。

通過觀察 CachedIntrospectionResults 可以看出,它也是調用 Introspector 來對類進行自省谈山。然后基于此并做了相應的緩存處理奏路。

    private static BeanInfo getBeanInfo(Class<?> beanClass) throws IntrospectionException {
        for (BeanInfoFactory beanInfoFactory : beanInfoFactories) {
            BeanInfo beanInfo = beanInfoFactory.getBeanInfo(beanClass);
            if (beanInfo != null) {
                return beanInfo;
            }
        }
        return (shouldIntrospectorIgnoreBeaninfoClasses ?
                Introspector.getBeanInfo(beanClass, Introspector.IGNORE_ALL_BEANINFO) :
                Introspector.getBeanInfo(beanClass));
    }

至此鸽粉,我們的 BeanWrapperImpl 已經具備了一定的自省能力触机。

private static void main(String[] args) {
  BeanWrapper beanWrapper = new BeanWrapperImpl(User.class);
  PropertyDescriptor[] propertyDescriptors = beanWrapper.getPropertyDescriptors();
  for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
    System.out.printf("%s : %s, %s\n", propertyDescriptor.getName(),
                      propertyDescriptor.getReadMethod(),
                      propertyDescriptor.getWriteMethod());
  }
}

輸出結果:

age : public java.lang.Integer com.grasswort.beans.model.User.getAge(), public void com.grasswort.beans.model.User.setAge(java.lang.Integer)
class : public final native java.lang.Class java.lang.Object.getClass(), null
id : public java.lang.Long com.grasswort.beans.model.User.getId(), public void com.grasswort.beans.model.User.setId(java.lang.Long)
name : public java.lang.String com.grasswort.beans.model.User.getName(), public void com.grasswort.beans.model.User.setName(java.lang.String)

PropertyAccessor:屬性存取能力

Java Doc

Common interface for classes that can access named properties (such as bean properties of an object or fields in an object) . Serves as base interface for {@link BeanWrapper}.

PropertyAccessor 作為 BeanWrapper 的一個基本接口儡首,定義了一系列對命名屬性的訪問和存儲方法椒舵。

  • 判斷屬性是否可讀约谈,可寫
    • isReadableProperty
    • isWritableProperty
  • 獲取屬性的類型
    • getPropertyType
    • getPropertyTypeDescriptor
  • 屬性值的獲取
    • getPropertyValue
  • 設置屬性值
    • setPropertyValue
    • setPropertyValues

通過這些方法棱诱,我們可以直接地去操作 Bean 的屬性迈勋。

private static void main(String[] args) {
  BeanWrapper beanWrapper = new BeanWrapperImpl(User.class);
  beanWrapper.setPropertyValue("id", "1");
  beanWrapper.setPropertyValue("name", "jerry");
  System.out.println("id : " + beanWrapper.getPropertyValue("id"));
  System.out.println("name : " + beanWrapper.getPropertyValue("name"));
  System.out.println(beanWrapper.getWrappedInstance());
}

輸出結果:

id : 1
name : jerry
User{id=1, name='jerry', age=null}

TypeConverter:類型轉換能力

以上靡菇,我們已經了解到米愿,我們可以通過 BeanWrappersetPropertyValue 方法來設置屬性的值育苟。但是上文示例代碼中使用的是:

beanWrapper.setPropertyValue("id", "1");

而在 User.java 中违柏,id 聲明的卻是 Long 類型:

private Long id;

而之所以能夠設置屬性值成功香椎,則說明在賦值的過程中畜伐,一定存在一個類型轉換的過程玛界。

BeanWrapperImpl 之所以具備類型轉換能力,是因為它繼承自 TypeConverterSupportTypeConverterSupport 組合了一個 TypeConverterDelegate 委派對象鲤脏,類型轉換邏輯將交由 TypeConvertDelegate 去執(zhí)行操作猎醇。
為了使頭腦中對此處結構更加清晰努溃,特意對 BeanWrapperImpl 類圖進行了一次重新整理:

在圖中心偏上位置的是一個 PropertyEditorRegistrySupport 類梧税。它提供了 PropertyEditorRegistry 的具體實現(xiàn)第队,支持對基于 JavaBeansjdk1.1 提供的類型轉換接口 PropertyEditor 進行注冊和擴展凳谦。除此之外,它還包含了一個可選的(@NullableConversionService 的成員變量家凯,提供了對 spring 3 的一系列spring 自己提供的類型轉換接口的支持與擴展绊诲。

  • 基礎的 PropertyEditor

  • 可選的 ConversionService

而具體的轉換邏輯驯镊,則在 TypeConvertDelegate 類中進行了體現(xiàn)。

@Nullable
    public <T> T convertIfNecessary(@Nullable String propertyName, @Nullable Object oldValue, @Nullable Object newValue,
            @Nullable Class<T> requiredType, @Nullable TypeDescriptor typeDescriptor) throws IllegalArgumentException {

    // 先嘗試尋找自定義的 PropertyEditor
        PropertyEditor editor = this.propertyEditorRegistry.findCustomEditor(requiredType, propertyName);

    // 如果沒有找到對應的 PropertyEditor橄镜,并且 ConversionService 不為空洽胶。則嘗試使用 ConversionService 去轉換姊氓。
        ConversionService conversionService = this.propertyEditorRegistry.getConversionService();
        if (editor == null && conversionService != null && newValue != null && typeDescriptor != null) {
            // ...
        }

        Object convertedValue = newValue;

        // 如果找到了相應的 PropertyEditor
    // 或者沒找到翔横,但是傳入類型與要求類型不符梗搅,則尋找默認 PropertyEditor 進行轉換
        if (editor != null 
        || (requiredType != null && !ClassUtils.isAssignableValue(requiredType, convertedValue))) {
            // ...
        }

        // 如果還沒轉換成功无切,則嘗試應用一些標準類型轉換規(guī)則哆键。
    // 包含數(shù)字、枚舉闪盔、數(shù)組锭沟、集合等的處理识补。
  }

OK,那么我們已經了解到類型轉換可以有兩種擴展方式:

  • 基于 JavaBeansPropertyEditor
  • Spring 3 提供的 core.convert 包下的新接口贴妻,包含 Converter<S, T>名惩、GenericConverter孕荠、ConverterFactory<S, T>

PropertyEditor:基于 JavaBeans 的類型轉換接口

PropertyEditor 位于 java.beans 包下弯予,支持將 String 類型的對象轉換成其他任意類型的對象个曙。

通常我們是通過 setAsText(String text) 方法來傳入一個字符串垦搬,然后經過自定義的處理邏輯后調用 setValue(Object obj) 方法猴贰,將轉換后的值存儲起來,客戶端便可以通過 getValue() 方法獲取轉換后的值瑟捣。

另外調用setValue(Object obj)方法的時候會觸發(fā)一個PropertyChangeEvent事件蝶柿,通知所有監(jiān)聽的 PropertyChangeListener

通常雏赦,我們創(chuàng)建一個自定義的 PropertyEditor 時星岗,并不需要直接實現(xiàn) PropertyEditor 接口俏橘。而是通過繼承 PropertyEditorSupport 類,然后選擇性地重寫 setAsText(String text)getAsText() 方法即可靴寂。

例如:

public class StringToUserPropertyEditor extends PropertyEditorSupport {
    // 例如: 1-jerry-8

    @Override
    public String getAsText() {
        User user = (User) getValue();
        return user.getId() + "-" + user.getName() + "-" + user.getAge();
    }

    @Override
    public void setAsText(String text) throws IllegalArgumentException {
        String[] strArray = text.split("-");
        User user = new User();
        user.setId(Long.valueOf(strArray[0]));
        user.setName(strArray[1]);
        user.setAge(Integer.valueOf(strArray[2]));
        setValue(user);
    }

    public static void main(String[] args) {
        String text = "1-jerry-8";
        StringToUserPropertyEditor editor = new StringToUserPropertyEditor();
        editor.setAsText(text);
        User user = (User) editor.getValue();
        System.out.println(user);
    }
}

輸出結果為:

User{id=1, name='jerry', age=8}

PropertyEditor 雖然具備了類型轉換的能力褐隆,卻也存在著一定的局限性庶弃。

  1. 它的來源類型只能是 String 類型德澈。
  2. 它的實現(xiàn)缺少類型安全圃验,實現(xiàn)類無法感知目標轉換類型澳窑。
  3. 它的實現(xiàn)使用了成員變量 value 來存儲轉換結果摊聋,線程不安全。
  4. 除了類型轉換為箍镜,它的實現(xiàn)了還包含了 JavaBeans 時間以及 JavaGUI 交互(在以上的 UML 類圖中省略了這部分方法)色迂。違反了職責單一原則歇僧。

因此锋拖, Spring 3 又提供了一些新的類型轉換接口支持兽埃。

Spring 3 core.convert 通用類型轉換

  • Convert<S, T>

Spring 3 之后柄错,提供了這樣一個接口 Convert<S, T>

S 代表了來源數(shù)據(jù)類型躏啰, T 則代表了目標轉換類型给僵。通過 Java 泛型限制了方法的參數(shù)類型和返回類型帝际。解決了 PropertyEditor 的類型不安全問題饶辙,同時 T convert(S source) 的方法設計弃揽,相比于 PropertyEditor需要先存儲后取值的接口方法設計矿微,更有助于其實現(xiàn)類實現(xiàn)一個線程安全的類型轉換器涌矢。

該接口實現(xiàn)非常的簡單,且容易理解塔次,便不再舉例說明励负。

但它仍然存在一些小的問題匕得,由于泛型擦除的原因耗跛,當我們注冊了許多 Converter 的時候调塌,我們不可能去一一進行轉換嘗試羔砾。

所以 Spring 又提供了一個 ConditionalConvert 接口姜凄。

public interface ConditionalConverter {
   boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType);
}

我們的實現(xiàn)類可以同時實現(xiàn)這兩個接口:ConverterConditionalConverter董虱。在轉化過程中會先去調用 matches 方法,來判斷是否匹配申鱼,匹配的話才會調用 convert 方法進行轉換愤诱。

public class PropertiesToUserConverter implements Converter<Properties, User>, ConditionalConverter {

    @Override
    public User convert(Properties source) {
        User user = new User();
        user.setId(Long.valueOf(source.getProperty("id", "0L")));
        user.setName(String.valueOf(source.getOrDefault("name", "")));
        user.setAge(Integer.valueOf(source.getProperty("age", "0")));
        return user;
    }

    @Override
    public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
        return sourceType.getObjectType().isAssignableFrom(Properties.class)
                && targetType.getObjectType().isAssignableFrom(User.class);
    }

    public static void main(String[] args) {
        Properties properties = new Properties();
        properties.setProperty("id", "1");
        properties.setProperty("name", "jerry");
        properties.setProperty("age", "8");
        PropertiesToUserConverter converter = new PropertiesToUserConverter();
        boolean matched = converter.matches(TypeDescriptor.forObject(properties), TypeDescriptor.valueOf(User.class));
        if (matched) {
            User user = converter.convert(properties);
            System.out.println(user);
        }
    }
}
  • GenericConverter

Spring 還提供了一個支持復雜類型轉換的接口:GenericConverter。它相比于 Converter<S, T> 更加靈活捐友。

可以通過 getConvertibleTypes() 返回一個 ConvertiblePair 集合淫半。每一個 ConvertiblePair 都包含了一個 soureTypetargetType ,表示可轉換類型對匣砖。

GenericConverter 可以與 Converter 相配合,對復雜的數(shù)據(jù)類型進行轉換猴鲫。通扯匀耍可以用于對集合類型的對象進行轉換 。

具體可以查看相應的實現(xiàn)類來查看拂共。


Spring 官方文檔:

Because GenericConverter is a more complex SPI interface, you should use it only when you need it. Favor Converter or ConverterFactory for basic type conversion needs.

不過规伐,GenericConverter 作為通用類型轉換器雖然功能強大,但只有需要的時候才去使用它匣缘。如果可以猖闪,還是要優(yōu)先考慮 Converter 或者 ConverterFactory

ConversionService

core.convert 包下肌厨,還存在這樣一個類 ConversionService培慌。

public interface ConversionService {

    boolean canConvert(@Nullable Class<?> sourceType, Class<?> targetType);

    boolean canConvert(@Nullable TypeDescriptor sourceType, TypeDescriptor targetType);

    @Nullable
    <T> T convert(@Nullable Object source, Class<T> targetType);

    @Nullable
    Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType);

}

Spring 官方文檔:

A ConversionService is a stateless object designed to be instantiated at application startup and then shared between multiple threads. In a Spring application, you typically configure a ConversionService instance for each Spring container (or ApplicationContext). Spring picks up that ConversionService and uses it whenever a type conversion needs to be performed by the framework. You can also inject this ConversionService into any of your beans and invoke it directly.

JavaDoc :

A service interface for type conversion. This is the entry point into the convert system.

Call {@link #convert(Object, Class)} to perform a thread-safe type conversion using this system.

JavaDoc 所言,這是一個類型轉換服務入口類(Spring 3 core.convert)柑爸。通常吵护,我們使用這個類來做類型轉換即可。

它的實現(xiàn)類實現(xiàn)了 ConverterRegistry 接口表鳍,支持 Converter馅而、ConverterFactoryGenericConverter 的注冊與擴展譬圣。

Spring 應用程序中瓮恭,我們可以通過 ConversionServiceFactoryBean 來將 ConversionService 注冊到容器中。但是需要注意的是 id 必須為 conversionService厘熟。

<bean id="conversionService"
        class="org.springframework.context.support.ConversionServiceFactoryBean">
    <property name="converters">
        <set>
            <bean class="example.MyCustomConverter"/>
        </set>
    </property>
</bean>
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末屯蹦,一起剝皮案震驚了整個濱河市维哈,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌登澜,老刑警劉巖阔挠,帶你破解...
    沈念sama閱讀 219,490評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異脑蠕,居然都是意外死亡购撼,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評論 3 395
  • 文/潘曉璐 我一進店門谴仙,熙熙樓的掌柜王于貴愁眉苦臉地迎上來份招,“玉大人,你說我怎么就攤上這事狞甚∷ぃ” “怎么了?”我有些...
    開封第一講書人閱讀 165,830評論 0 356
  • 文/不壞的土叔 我叫張陵哼审,是天一觀的道長谐腰。 經常有香客問我,道長涩盾,這世上最難降的妖魔是什么十气? 我笑而不...
    開封第一講書人閱讀 58,957評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮春霍,結果婚禮上砸西,老公的妹妹穿的比我還像新娘。我一直安慰自己址儒,他們只是感情好芹枷,可當我...
    茶點故事閱讀 67,974評論 6 393
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著莲趣,像睡著了一般鸳慈。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上喧伞,一...
    開封第一講書人閱讀 51,754評論 1 307
  • 那天走芋,我揣著相機與錄音,去河邊找鬼潘鲫。 笑死翁逞,一個胖子當著我的面吹牛,可吹牛的內容都是我干的溉仑。 我是一名探鬼主播挖函,決...
    沈念sama閱讀 40,464評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼彼念!你這毒婦竟也來了挪圾?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤逐沙,失蹤者是張志新(化名)和其女友劉穎哲思,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體吩案,經...
    沈念sama閱讀 45,847評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡棚赔,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,995評論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了徘郭。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片靠益。...
    茶點故事閱讀 40,137評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖残揉,靈堂內的尸體忽然破棺而出胧后,到底是詐尸還是另有隱情,我是刑警寧澤抱环,帶...
    沈念sama閱讀 35,819評論 5 346
  • 正文 年R本政府宣布壳快,位于F島的核電站,受9級特大地震影響镇草,放射性物質發(fā)生泄漏眶痰。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,482評論 3 331
  • 文/蒙蒙 一梯啤、第九天 我趴在偏房一處隱蔽的房頂上張望竖伯。 院中可真熱鬧,春花似錦因宇、人聲如沸七婴。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,023評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽本姥。三九已至,卻和暖如春杭棵,著一層夾襖步出監(jiān)牢的瞬間婚惫,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,149評論 1 272
  • 我被黑心中介騙來泰國打工魂爪, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留先舷,地道東北人。 一個月前我還...
    沈念sama閱讀 48,409評論 3 373
  • 正文 我出身青樓滓侍,卻偏偏與公主長得像蒋川,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子撩笆,可洞房花燭夜當晚...
    茶點故事閱讀 45,086評論 2 355