資料繫結
資料繫結對於將使用者輸入繫結到目標物件非常有用,其中使用者輸入是一個以屬性路徑為鍵的對映,遵循 JavaBeans 約定。DataBinder
是支援此功能的主要類,它提供了兩種繫結使用者輸入的方法:
你可以同時應用建構函式繫結和屬性繫結,或者只應用其中一種。
建構函式繫結
要使用建構函式繫結:
-
建立一個目標物件為
null
的DataBinder
。 -
將
targetType
設定為目標類。 -
呼叫
construct
方法。
目標類應具有一個公共建構函式,或一個帶有引數的非公共建構函式。如果存在多個建構函式,則使用預設建構函式(如果存在)。
預設情況下,引數值透過建構函式引數名稱查詢。如果存在,Spring MVC 和 WebFlux 支援透過建構函式引數或欄位上的 @BindParam
註解進行自定義名稱對映。如有必要,您還可以透過在 DataBinder
上配置 NameResolver
來自定義要使用的引數名稱。
型別轉換 會根據需要應用以轉換使用者輸入。如果建構函式引數是一個物件,它會以相同的方式遞迴構建,但透過巢狀屬性路徑。這意味著建構函式繫結既建立目標物件,也建立它包含的任何物件。
建構函式繫結支援 List
、Map
和陣列引數,這些引數可以從單個字串轉換而來(例如,逗號分隔列表),也可以基於索引鍵(例如 accounts[2].name
或 account[KEY].name
)。
繫結和轉換錯誤會反映在 DataBinder
的 BindingResult
中。如果目標物件成功建立,則在呼叫 construct
後,target
將設定為建立的例項。
使用 BeanWrapper
進行屬性繫結
org.springframework.beans
包遵循 JavaBeans 標準。JavaBean 是一個具有預設無參建構函式並遵循命名約定的類,例如,一個名為 bingoMadness
的屬性將有一個 setter 方法 setBingoMadness(..)
和一個 getter 方法 getBingoMadness()
。有關 JavaBeans 和規範的更多資訊,請參閱 javabeans。
beans 包中一個非常重要的類是 BeanWrapper
介面及其相應的實現 (BeanWrapperImpl
)。正如 javadoc 中引用的,BeanWrapper
提供了設定和獲取屬性值(單獨或批次)、獲取屬性描述符以及查詢屬性以確定它們是否可讀或可寫的功能。此外,BeanWrapper
還支援巢狀屬性,可以無限深度地設定子屬性上的屬性。BeanWrapper
還支援新增標準的 JavaBeans PropertyChangeListeners
和 VetoableChangeListeners
,而無需在目標類中編寫支援程式碼。最後但並非最不重要的一點是,BeanWrapper
提供了設定索引屬性的支援。應用程式程式碼通常不直接使用 BeanWrapper
,而是由 DataBinder
和 BeanFactory
使用它。
BeanWrapper
的工作方式部分地由其名稱表明:它包裝一個 bean,以便對該 bean 執行操作,例如設定和檢索屬性。
設定和獲取基本屬性和巢狀屬性
設定和獲取屬性是透過 BeanWrapper
的 setPropertyValue
和 getPropertyValue
方法的過載變體完成的。有關詳細資訊,請參閱它們的 Javadoc。下表顯示了一些遵循這些約定的示例:
表示式 | 說明 |
---|---|
|
表示對應於 |
|
表示屬性 |
|
表示索引屬性 |
|
表示由 |
(如果您不打算直接使用 BeanWrapper
,那麼下一節對您來說不是至關重要的。如果您只使用 DataBinder
和 BeanFactory
及其預設實現,則應跳到關於 PropertyEditors
的部分。)
以下兩個示例類使用 BeanWrapper
來獲取和設定屬性:
-
Java
-
Kotlin
public class Company {
private String name;
private Employee managingDirector;
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public Employee getManagingDirector() {
return this.managingDirector;
}
public void setManagingDirector(Employee managingDirector) {
this.managingDirector = managingDirector;
}
}
class Company {
var name: String? = null
var managingDirector: Employee? = null
}
-
Java
-
Kotlin
public class Employee {
private String name;
private float salary;
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public float getSalary() {
return salary;
}
public void setSalary(float salary) {
this.salary = salary;
}
}
class Employee {
var name: String? = null
var salary: Float? = null
}
以下程式碼片段展示瞭如何檢索和操作例項化後的 Company
和 Employee
物件的一些屬性:
-
Java
-
Kotlin
BeanWrapper company = new BeanWrapperImpl(new Company());
// setting the company name..
company.setPropertyValue("name", "Some Company Inc.");
// ... can also be done like this:
PropertyValue value = new PropertyValue("name", "Some Company Inc.");
company.setPropertyValue(value);
// ok, let's create the director and tie it to the company:
BeanWrapper jim = new BeanWrapperImpl(new Employee());
jim.setPropertyValue("name", "Jim Stravinsky");
company.setPropertyValue("managingDirector", jim.getWrappedInstance());
// retrieving the salary of the managingDirector through the company
Float salary = (Float) company.getPropertyValue("managingDirector.salary");
val company = BeanWrapperImpl(Company())
// setting the company name..
company.setPropertyValue("name", "Some Company Inc.")
// ... can also be done like this:
val value = PropertyValue("name", "Some Company Inc.")
company.setPropertyValue(value)
// ok, let's create the director and tie it to the company:
val jim = BeanWrapperImpl(Employee())
jim.setPropertyValue("name", "Jim Stravinsky")
company.setPropertyValue("managingDirector", jim.wrappedInstance)
// retrieving the salary of the managingDirector through the company
val salary = company.getPropertyValue("managingDirector.salary") as Float?
PropertyEditor
Spring 使用 PropertyEditor
的概念來實現 Object
和 String
之間的轉換。以與物件本身不同的方式表示屬性會很方便。例如,Date
可以以人類可讀的方式表示(如 String
:'2007-14-09'
),同時我們仍然可以將人類可讀的形式轉換回原始日期(或者更好,將任何以人類可讀形式輸入的日期轉換回 Date
物件)。這種行為可以透過註冊型別為 java.beans.PropertyEditor
的自定義編輯器來實現。在 BeanWrapper
或特定 IoC 容器(如前一章所述)中註冊自定義編輯器,使其具備將屬性轉換為所需型別的能力。有關 PropertyEditor
的更多資訊,請參閱 Oracle 的 java.beans
包的 javadoc。
Spring 中使用屬性編輯的幾個例子:
-
透過使用
PropertyEditor
實現來設定 bean 的屬性。當您在 XML 檔案中宣告某個 bean 的屬性值使用String
型別時,Spring(如果相應屬性的 setter 方法有一個Class
引數)會使用ClassEditor
嘗試將引數解析為Class
物件。 -
在 Spring 的 MVC 框架中解析 HTTP 請求引數是透過使用各種
PropertyEditor
實現來完成的,您可以在CommandController
的所有子類中手動繫結它們。
Spring 內建了許多 PropertyEditor
實現,以簡化開發。它們都位於 org.springframework.beans.propertyeditors
包中。大多數(但並非全部,如下表所示)預設情況下由 BeanWrapperImpl
註冊。如果屬性編輯器可以在某種程度上配置,您仍然可以註冊自己的變體來覆蓋預設編輯器。下表描述了 Spring 提供的各種 PropertyEditor
實現:
類 | 說明 |
---|---|
|
位元組陣列的編輯器。將字串轉換為相應的位元組表示。預設由 |
|
將表示類的字串解析為實際的類,反之亦然。當找不到類時,會丟擲 |
|
用於 |
|
集合的屬性編輯器,將任何源 |
|
用於 |
|
用於任何 |
|
將字串解析為 |
|
單向屬性編輯器,可以接受字串並透過中間的 |
|
可以將字串解析為 |
|
可以將字串解析為 |
|
可以將字串(使用 |
|
去除字串首尾空白的屬性編輯器。可選地允許將空字串轉換為 |
|
可以將 URL 的字串表示解析為實際的 |
Spring 使用 java.beans.PropertyEditorManager
來設定可能需要的屬性編輯器的搜尋路徑。搜尋路徑還包括 sun.bean.editors
,其中包含用於 Font
、Color
和大多數原始型別等型別的 PropertyEditor
實現。另請注意,標準 JavaBeans 基礎設施會自動發現 PropertyEditor
類(無需顯式註冊),如果它們與它們處理的類位於同一包中,並且與該類同名,只是附加了 Editor
字尾。例如,可以有以下類和包結構,這足以使 SomethingEditor
類被識別並用作 Something
型別屬性的 PropertyEditor
。
com chank pop Something SomethingEditor // the PropertyEditor for the Something class
請注意,您這裡也可以使用標準的 BeanInfo
JavaBeans 機制(在 這裡 有一定程度的描述)。以下示例使用 BeanInfo
機制為關聯類的屬性顯式註冊一個或多個 PropertyEditor
例項:
com chank pop Something SomethingBeanInfo // the BeanInfo for the Something class
以下是引用的 SomethingBeanInfo
類的 Java 原始碼,它將 CustomNumberEditor
與 Something
類的 age
屬性關聯起來:
-
Java
-
Kotlin
public class SomethingBeanInfo extends SimpleBeanInfo {
public PropertyDescriptor[] getPropertyDescriptors() {
try {
final PropertyEditor numberPE = new CustomNumberEditor(Integer.class, true);
PropertyDescriptor ageDescriptor = new PropertyDescriptor("age", Something.class) {
@Override
public PropertyEditor createPropertyEditor(Object bean) {
return numberPE;
}
};
return new PropertyDescriptor[] { ageDescriptor };
}
catch (IntrospectionException ex) {
throw new Error(ex.toString());
}
}
}
class SomethingBeanInfo : SimpleBeanInfo() {
override fun getPropertyDescriptors(): Array<PropertyDescriptor> {
try {
val numberPE = CustomNumberEditor(Int::class.java, true)
val ageDescriptor = object : PropertyDescriptor("age", Something::class.java) {
override fun createPropertyEditor(bean: Any): PropertyEditor {
return numberPE
}
}
return arrayOf(ageDescriptor)
} catch (ex: IntrospectionException) {
throw Error(ex.toString())
}
}
}
自定義 PropertyEditor
將 bean 屬性設定為字串值時,Spring IoC 容器最終使用標準的 JavaBeans PropertyEditor
實現將這些字串轉換為屬性的複雜型別。Spring 預先註冊了許多自定義的 PropertyEditor
實現(例如,將表示類名的字串轉換為 Class
物件)。此外,Java 的標準 JavaBeans PropertyEditor
查詢機制允許為類指定適當名稱並將 PropertyEditor
放置在與其支援的類相同的包中,以便可以自動找到它。
如果需要註冊其他自定義 PropertyEditors
,有幾種機制可用。最手動的方法(通常不方便或不推薦)是使用 ConfigurableBeanFactory
介面的 registerCustomEditor()
方法,前提是您有一個 BeanFactory
引用。另一種(稍微方便一點)的機制是使用一個特殊的 bean 工廠後處理器,稱為 CustomEditorConfigurer
。儘管您可以將 bean 工廠後處理器與 BeanFactory
實現一起使用,但 CustomEditorConfigurer
具有巢狀屬性設定,因此我們強烈建議您將其與 ApplicationContext
一起使用,在那裡您可以像部署任何其他 bean 一樣部署它,並且它可以被自動檢測和應用。
請注意,所有 bean 工廠和應用上下文都會透過使用 BeanWrapper
來處理屬性轉換,從而自動使用許多內建的屬性編輯器。BeanWrapper
註冊的標準屬性編輯器列在 上一節 中。此外,ApplicationContext
還會覆蓋或新增額外的編輯器,以適當的方式處理資源查詢,以適應特定的應用上下文型別。
標準的 JavaBeans PropertyEditor
例項用於將表示為字串的屬性值轉換為屬性的實際複雜型別。您可以使用 CustomEditorConfigurer
(一個 bean 工廠後處理器)方便地為 ApplicationContext
新增對額外 PropertyEditor
例項的支援。
考慮以下示例,它定義了一個名為 ExoticType
的使用者類,以及另一個名為 DependsOnExoticType
的類,後者需要將 ExoticType
設定為一個屬性:
-
Java
-
Kotlin
package example;
public class ExoticType {
private String name;
public ExoticType(String name) {
this.name = name;
}
}
public class DependsOnExoticType {
private ExoticType type;
public void setType(ExoticType type) {
this.type = type;
}
}
package example
class ExoticType(val name: String)
class DependsOnExoticType {
var type: ExoticType? = null
}
正確設定後,我們希望能夠將 type 屬性指定為字串,由 PropertyEditor
將其轉換為實際的 ExoticType
例項。以下 bean 定義顯示瞭如何設定這種關係:
<bean id="sample" class="example.DependsOnExoticType">
<property name="type" value="aNameForExoticType"/>
</bean>
PropertyEditor
實現可能看起來類似於以下內容:
-
Java
-
Kotlin
package example;
import java.beans.PropertyEditorSupport;
// converts string representation to ExoticType object
public class ExoticTypeEditor extends PropertyEditorSupport {
public void setAsText(String text) {
setValue(new ExoticType(text.toUpperCase()));
}
}
package example
import java.beans.PropertyEditorSupport
// converts string representation to ExoticType object
class ExoticTypeEditor : PropertyEditorSupport() {
override fun setAsText(text: String) {
value = ExoticType(text.toUpperCase())
}
}
最後,以下示例展示瞭如何使用 CustomEditorConfigurer
向 ApplicationContext
註冊新的 PropertyEditor
,然後 ApplicationContext
就可以根據需要使用它了:
<bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
<property name="customEditors">
<map>
<entry key="example.ExoticType" value="example.ExoticTypeEditor"/>
</map>
</property>
</bean>
PropertyEditorRegistrar
向 Spring 容器註冊屬性編輯器的另一種機制是建立和使用 PropertyEditorRegistrar
。當您需要在多種不同情況下使用同一組屬性編輯器時,此介面特別有用。您可以編寫相應的註冊器並在每種情況下重用它。PropertyEditorRegistrar
例項與一個名為 PropertyEditorRegistry
的介面協同工作,該介面由 Spring 的 BeanWrapper
(和 DataBinder
)實現。PropertyEditorRegistrar
例項與 CustomEditorConfigurer
(此處 描述)結合使用時特別方便,後者暴露了一個名為 setPropertyEditorRegistrars(..)
的屬性。以這種方式新增到 CustomEditorConfigurer
的 PropertyEditorRegistrar
例項可以輕鬆地與 DataBinder
和 Spring MVC 控制器共享。此外,它避免了自定義編輯器上的同步需求:PropertyEditorRegistrar
預計為每次 bean 建立嘗試建立全新的 PropertyEditor
例項。
以下示例展示瞭如何建立自己的 PropertyEditorRegistrar
實現:
-
Java
-
Kotlin
package com.foo.editors.spring;
public final class CustomPropertyEditorRegistrar implements PropertyEditorRegistrar {
public void registerCustomEditors(PropertyEditorRegistry registry) {
// it is expected that new PropertyEditor instances are created
registry.registerCustomEditor(ExoticType.class, new ExoticTypeEditor());
// you could register as many custom property editors as are required here...
}
}
package com.foo.editors.spring
import org.springframework.beans.PropertyEditorRegistrar
import org.springframework.beans.PropertyEditorRegistry
class CustomPropertyEditorRegistrar : PropertyEditorRegistrar {
override fun registerCustomEditors(registry: PropertyEditorRegistry) {
// it is expected that new PropertyEditor instances are created
registry.registerCustomEditor(ExoticType::class.java, ExoticTypeEditor())
// you could register as many custom property editors as are required here...
}
}
另請參閱 org.springframework.beans.support.ResourceEditorRegistrar
以獲取 PropertyEditorRegistrar
實現的示例。請注意在其 registerCustomEditors(..)
方法實現中,它如何為每個屬性編輯器建立新的例項。
下一個示例展示瞭如何配置 CustomEditorConfigurer
並將我們的 CustomPropertyEditorRegistrar
例項注入其中:
<bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
<property name="propertyEditorRegistrars">
<list>
<ref bean="customPropertyEditorRegistrar"/>
</list>
</property>
</bean>
<bean id="customPropertyEditorRegistrar"
class="com.foo.editors.spring.CustomPropertyEditorRegistrar"/>
最後(稍微偏離本章重點),對於使用 Spring MVC web 框架 的使用者,將 PropertyEditorRegistrar
與資料繫結 web 控制器結合使用會非常方便。以下示例在 @InitBinder
方法的實現中使用了 PropertyEditorRegistrar
:
-
Java
-
Kotlin
@Controller
public class RegisterUserController {
private final PropertyEditorRegistrar customPropertyEditorRegistrar;
RegisterUserController(PropertyEditorRegistrar propertyEditorRegistrar) {
this.customPropertyEditorRegistrar = propertyEditorRegistrar;
}
@InitBinder
void initBinder(WebDataBinder binder) {
this.customPropertyEditorRegistrar.registerCustomEditors(binder);
}
// other methods related to registering a User
}
@Controller
class RegisterUserController(
private val customPropertyEditorRegistrar: PropertyEditorRegistrar) {
@InitBinder
fun initBinder(binder: WebDataBinder) {
this.customPropertyEditorRegistrar.registerCustomEditors(binder)
}
// other methods related to registering a User
}
這種 PropertyEditor
註冊風格可以帶來簡潔的程式碼(@InitBinder
方法的實現只有一行長),並且可以將通用的 PropertyEditor
註冊程式碼封裝在一個類中,然後在需要時在多個控制器之間共享。