示例代碼
參考文檔
- Spring 官方文檔
- 小馬哥:Spring 核心編程思想
涉及問題
- 什么是數(shù)據(jù)綁定(
Data Binding
)凛虽? -
DataBinder
與BeanWrapper
之間的關系? -
BeanWrapper
都具備哪些功能 殃姓? - 數(shù)據(jù)綁定過程中的類型轉換(
TypeConvert
)是怎么實現(xiàn)的瓦阐?
DataBinder
- 什么是數(shù)據(jù)綁定呢睡蟋?
Spring 官方文檔:
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. TheValidator
and theDataBinder
make up thevalidation
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
的集合档址。我們可以通過 DataBinder
的 bind(propertyValues)
方法來將一個或多個屬性值綁定到目標對象上。
通過對 bind
方法進行 debug
分析绎秒,我們發(fā)現(xiàn)實際上 DataBinder
調用的是一個更底層的類 BeanWrapperImpl
來實現(xiàn)對 Bean
的操作的见芹。
BeanWrapper
:一個底層的 Bean 操作類
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 theBeanWrapper
directly.
如官方文檔所言玄呛,BeanWrapper
作為一個 spring
框架的一個底層基礎工具(通常并不需要直接去使用它)和二,運用于多個地方儿咱。例如:DataBinder
以及 BeanFactory
。
接下來怠缸,我們便對 BeanWrapper
進行一個詳細的分析:
首先揭北,我們先看一下 BeanWrapper
的默認實現(xiàn)類 BeanWrapperImpl
的類繼承結構圖:
我們重點關注這三個接口搔体,先對它們有一個直觀的了解,然后再逐個分析:
BeanWrapper
提供了包裝 Bean
的能力劝术,以及對所封裝 Bean
的自省能力养晋。(自省是指自我觀察獲取自身內部結構梁钾,自我描述的能力)
PropertyAccessor
提供了直觀的操作屬性的能力姆泻。
TypeConverter
提供了類型轉換的能力。
那么闭翩,總結一下蛔琅,BeanWrapperImpl
是一個可以包裝一個目標對象罗售,并可以對目標對象進行自省以及屬性賦值的工具類钩述。并且在屬性賦值的過程中支持屬性值的類型轉換牙勘。那么 BeanWrapperImpl
便具備以下能力:
- 自省能力
- 屬性存取能力
- 類型轉化能力
下面呢,將會對這些能力的實現(xiàn)逐一進行更詳細的分析放钦。
Bean Introspection
: 自省能力
Bean Introspection
是指 Bean
的自省操禀。
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 namedbingoMadness
would have a setter methodsetBingoMadness(..)
and a getter methodgetBingoMadness()
. For more information about JavaBeans and the specification, see javabeans.
Spring
的 org.springframework.beans
包遵守了 JavaBeans
標準颓屑。一個 JavaBean
呢,是指一個具備默認構造函數(shù)遍搞,并且遵守一定命名規(guī)約的類溪猿。比如纫塌,我們平日里熟知的 getter
和 setter
方法护戳,則分別對應了相關屬性的讀方法和寫方法媳荒。
在 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
:類型轉換能力
以上靡菇,我們已經了解到米愿,我們可以通過 BeanWrapper
的 setPropertyValue
方法來設置屬性的值育苟。但是上文示例代碼中使用的是:
beanWrapper.setPropertyValue("id", "1");
而在 User.java
中违柏,id
聲明的卻是 Long
類型:
private Long id;
而之所以能夠設置屬性值成功香椎,則說明在賦值的過程中畜伐,一定存在一個類型轉換的過程玛界。
BeanWrapperImpl
之所以具備類型轉換能力,是因為它繼承自 TypeConverterSupport
。TypeConverterSupport
組合了一個 TypeConverterDelegate
委派對象鲤脏,類型轉換邏輯將交由 TypeConvertDelegate
去執(zhí)行操作猎醇。
為了使頭腦中對此處結構更加清晰努溃,特意對 BeanWrapperImpl
類圖進行了一次重新整理:
在圖中心偏上位置的是一個 PropertyEditorRegistrySupport
類梧税。它提供了 PropertyEditorRegistry
的具體實現(xiàn)第队,支持對基于 JavaBeans
由 jdk1.1
提供的類型轉換接口 PropertyEditor
進行注冊和擴展凳谦。除此之外,它還包含了一個可選的(@Nullable
) ConversionService
的成員變量家凯,提供了對 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,那么我們已經了解到類型轉換可以有兩種擴展方式:
- 基于
JavaBeans
的PropertyEditor
-
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
雖然具備了類型轉換的能力褐隆,卻也存在著一定的局限性庶弃。
- 它的來源類型只能是
String
類型德澈。 - 它的實現(xiàn)缺少類型安全圃验,實現(xiàn)類無法感知目標轉換類型澳窑。
- 它的實現(xiàn)使用了成員變量
value
來存儲轉換結果摊聋,線程不安全。 - 除了類型轉換為箍镜,它的實現(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)這兩個接口:Converter
,ConditionalConverter
董虱。在轉化過程中會先去調用 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
都包含了一個 soureType
和 targetType
,表示可轉換類型對匣砖。
GenericConverter
可以與 Converter
相配合,對復雜的數(shù)據(jù)類型進行轉換猴鲫。通扯匀耍可以用于對集合類型的對象進行轉換 。
具體可以查看相應的實現(xiàn)類來查看拂共。
Spring 官方文檔:
Because
GenericConverter
is a more complex SPI interface, you should use it only when you need it. FavorConverter
orConverterFactory
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);
}
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 aConversionService
instance for each Spring container (orApplicationContext
). Spring picks up thatConversionService
and uses it whenever a type conversion needs to be performed by the framework. You can also inject thisConversionService
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
馅而、ConverterFactory
、GenericConverter
的注冊與擴展譬圣。
在 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>