資料綁定

資料綁定適用於將使用者輸入綁定到目標物件,其中使用者輸入是一個以屬性路徑作為鍵的 Map,並遵循 JavaBeans 慣例DataBinder 是支援此功能的主要類別,它提供兩種綁定使用者輸入的方式

  • 建構子綁定 - 將使用者輸入綁定到公用資料建構子,在使用者輸入中查找建構子引數值。

  • 屬性綁定 - 將使用者輸入綁定到 setter,將使用者輸入中的鍵與目標物件結構的屬性匹配。

您可以同時應用建構子和屬性綁定,或僅應用其中一種。

建構子綁定

要使用建構子綁定

  1. 建立一個以 null 作為目標物件的 DataBinder

  2. targetType 設定為目標類別。

  3. 呼叫 construct

目標類別應該具有單一公用建構子或單一帶有引數的非公用建構子。如果有多個建構子,則會使用預設建構子 (如果存在)。

預設情況下,引數值會透過建構子參數名稱查找。Spring MVC 和 WebFlux 透過建構子參數或欄位上的 @BindParam 註解支援自訂名稱對應 (如果存在)。如有必要,您也可以在 DataBinder 上組態 NameResolver 以自訂要使用的引數名稱。

型別轉換 會根據需要應用於轉換使用者輸入。如果建構子參數是一個物件,則會以相同方式遞迴建構它,但透過巢狀屬性路徑。這表示建構子綁定會建立目標物件及其包含的任何物件。

建構子綁定支援 ListMap 和陣列引數,這些引數可以從單一字串轉換而來 (例如,逗號分隔列表),或基於索引鍵 (例如 accounts[2].nameaccount[KEY].name)。

綁定和轉換錯誤會反映在 DataBinderBindingResult 中。如果目標物件成功建立,則在呼叫 construct 後,target 會設定為已建立的實例。

使用 BeanWrapper 進行屬性綁定

org.springframework.beans 套件遵循 JavaBeans 標準。JavaBean 是一個具有預設無引數建構子的類別,並遵循命名慣例,其中 (例如) 名為 bingoMadness 的屬性將具有 setter 方法 setBingoMadness(..) 和 getter 方法 getBingoMadness()。有關 JavaBeans 和規格的更多資訊,請參閱 javabeans

beans 套件中一個非常重要的類別是 BeanWrapper 介面及其對應的實作 (BeanWrapperImpl)。正如從 javadoc 中引用的,BeanWrapper 提供設定和取得屬性值 (個別或批量)、取得屬性描述器以及查詢屬性以確定它們是否可讀寫的功能。此外,BeanWrapper 提供對巢狀屬性的支援,允許將子屬性的屬性設定到無限深度。BeanWrapper 還支援新增標準 JavaBeans PropertyChangeListenersVetoableChangeListeners 的能力,而無需目標類別中的支援程式碼。最後但並非最不重要的一點是,BeanWrapper 提供對設定索引屬性的支援。BeanWrapper 通常不直接由應用程式碼使用,而是由 DataBinderBeanFactory 使用。

BeanWrapper 的工作方式部分由其名稱表示:它包裝一個 bean 以對該 bean 執行操作,例如設定和擷取屬性。

設定和取得基本和巢狀屬性

設定和取得屬性是透過 BeanWrappersetPropertyValuegetPropertyValue 多載方法變體完成的。有關詳細資訊,請參閱其 Javadoc。下表顯示了這些慣例的一些範例

表 1. 屬性範例
運算式 說明

name

指示與 getName()isName()setName(..) 方法對應的屬性 name

account.name

指示屬性 account 的巢狀屬性 name,該屬性與 (例如) getAccount().setName()getAccount().getName() 方法對應。

accounts[2]

指示索引屬性 account第三個元素。索引屬性的型別可以是 arraylist 或其他自然排序的集合。

accounts[KEY]

指示由 KEY 值索引的 Map 條目的值。

(如果您不打算直接使用 BeanWrapper,則下一個章節對您而言並非至關重要。如果您僅使用 DataBinderBeanFactory 及其預設實作,則應跳到關於 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
}

以下程式碼片段顯示了一些範例,說明如何擷取和操作已實例化的 CompanyEmployee 的某些屬性

  • 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's

Spring 使用 PropertyEditor 的概念來實現 ObjectString 之間的轉換。以不同於物件本身的方式表示屬性可能很方便。例如,Date 可以以人類可讀的方式表示 (如 String: '2007-14-09'),同時我們仍然可以將人類可讀的形式轉換回原始日期 (或者,更好的是,將以人類可讀形式輸入的任何日期轉換回 Date 物件)。此行為可以透過註冊 java.beans.PropertyEditor 型別的自訂編輯器來實現。在 BeanWrapper 上或在特定的 IoC 容器中 (如前一章所述) 註冊自訂編輯器,使其了解如何將屬性轉換為所需的型別。有關 PropertyEditor 的更多資訊,請參閱 Oracle 提供的 java.beans 套件的 javadoc

以下是一些在 Spring 中使用屬性編輯的範例

  • Bean 上的屬性設定是透過使用 PropertyEditor 實作完成的。當您使用 String 作為在 XML 檔案中宣告的某些 bean 的屬性值時,Spring (如果對應屬性的 setter 具有 Class 參數) 會使用 ClassEditor 嘗試將參數解析為 Class 物件。

  • 在 Spring 的 MVC 框架中剖析 HTTP 請求參數是透過使用您可以手動綁定在 CommandController 的所有子類別中的各種 PropertyEditor 實作完成的。

Spring 有許多內建的 PropertyEditor 實作,讓生活更輕鬆。它們都位於 org.springframework.beans.propertyeditors 套件中。大多數 (但並非全部,如下表所示) 預設由 BeanWrapperImpl 註冊。如果屬性編輯器在某種程度上是可組態的,您仍然可以註冊自己的變體來覆寫預設變體。下表描述了 Spring 提供的各種 PropertyEditor 實作

表 2. 內建 PropertyEditor 實作
類別 說明

ByteArrayPropertyEditor

位元組陣列的編輯器。將字串轉換為其對應的位元組表示形式。預設由 BeanWrapperImpl 註冊。

ClassEditor

剖析表示類別的字串為實際類別,反之亦然。當找不到類別時,會擲回 IllegalArgumentException。預設由 BeanWrapperImpl 註冊。

CustomBooleanEditor

Boolean 屬性的可自訂屬性編輯器。預設由 BeanWrapperImpl 註冊,但可以透過註冊其自訂實例作為自訂編輯器來覆寫。

CustomCollectionEditor

集合的屬性編輯器,將任何來源 Collection 轉換為給定的目標 Collection 型別。

CustomDateEditor

java.util.Date 的可自訂屬性編輯器,支援自訂 DateFormat。預設註冊。必須根據需要以適當的格式由使用者註冊。

CustomNumberEditor

任何 Number 子類別 (例如 IntegerLongFloatDouble) 的可自訂屬性編輯器。預設由 BeanWrapperImpl 註冊,但可以透過註冊其自訂實例作為自訂編輯器來覆寫。

FileEditor

將字串解析為 java.io.File 物件。預設由 BeanWrapperImpl 註冊。

InputStreamEditor

單向屬性編輯器,可以接受字串並產生 (透過中介 ResourceEditorResource) InputStream,以便可以直接將 InputStream 屬性設定為字串。請注意,預設用法不會為您關閉 InputStream。預設由 BeanWrapperImpl 註冊。

LocaleEditor

可以將字串解析為 Locale 物件,反之亦然 (字串格式為 [language]_[country]_[variant],與 LocaletoString() 方法相同)。也接受空格作為分隔符號,作為底線的替代方案。預設由 BeanWrapperImpl 註冊。

PatternEditor

可以將字串解析為 java.util.regex.Pattern 物件,反之亦然。

PropertiesEditor

可以將字串 (格式為 java.util.Properties 類別的 javadoc 中定義的格式) 轉換為 Properties 物件。預設由 BeanWrapperImpl 註冊。

StringTrimmerEditor

修剪字串的屬性編輯器。選擇性地允許將空字串轉換為 null 值。預設註冊 — 必須由使用者註冊。

URLEditor

可以將 URL 的字串表示形式解析為實際的 URL 物件。預設由 BeanWrapperImpl 註冊。

Spring 使用 java.beans.PropertyEditorManager 來設定可能需要的屬性編輯器的搜尋路徑。搜尋路徑也包括 sun.bean.editors,其中包含 FontColor 和大多數基本型別等型別的 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 原始碼將 CustomNumberEditorSomething 類別的 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's

當將 bean 屬性設定為字串值時,Spring IoC 容器最終會使用標準 JavaBeans PropertyEditor 實作將這些字串轉換為屬性的複雜型別。Spring 預先註冊了許多自訂 PropertyEditor 實作 (例如,將表示類別名稱的字串轉換為 Class 物件)。此外,Java 的標準 JavaBeans PropertyEditor 查找機制允許將類別的 PropertyEditor 適當地命名並放置在與它提供支援的類別相同的套件中,以便可以自動找到它。

如果需要註冊其他自訂 PropertyEditors,則可以使用多種機制。最手動的方法 (通常不方便或不建議使用) 是使用 ConfigurableBeanFactory 介面的 registerCustomEditor() 方法,前提是您具有 BeanFactory 參考。另一種 (稍微方便一點的) 機制是使用名為 CustomEditorConfigurer 的特殊 bean 工廠後處理器。雖然您可以將 bean 工廠後處理器與 BeanFactory 實作搭配使用,但 CustomEditorConfigurer 具有巢狀屬性設定,因此我們強烈建議您將其與 ApplicationContext 搭配使用,您可以在其中以與任何其他 bean 類似的方式部署它,並且可以自動偵測和應用它。

請注意,所有 bean 工廠和應用程式 Context 都會透過使用 BeanWrapper 來處理屬性轉換,從而自動使用許多內建的屬性編輯器。BeanWrapper 註冊的標準屬性編輯器列在前一節中。此外,ApplicationContext 也會覆寫或新增其他編輯器,以適當於特定應用程式 Context 型別的方式處理資源查找。

標準 JavaBeans PropertyEditor 實例用於將表示為字串的屬性值轉換為屬性的實際複雜型別。您可以使用 bean 工廠後處理器 CustomEditorConfigurer,方便地為 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())
	}
}

最後,以下範例展示如何使用 CustomEditorConfigurerApplicationContext 註冊新的 PropertyEditor,以便在需要時使用它。

<bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
	<property name="customEditors">
		<map>
			<entry key="example.ExoticType" value="example.ExoticTypeEditor"/>
		</map>
	</property>
</bean>

PropertyEditorRegistrar

另一種向 Spring 容器註冊屬性編輯器 (property editor) 的機制是建立並使用 PropertyEditorRegistrar。當您需要在多種不同情況下使用相同的屬性編輯器集合時,此介面特別有用。您可以編寫一個對應的 registrar 并在每種情況下重複使用它。PropertyEditorRegistrar 實例與一個名為 PropertyEditorRegistry 的介面協同工作,該介面由 Spring 的 BeanWrapper (和 DataBinder) 實作。當與 CustomEditorConfigurer (在此描述) 結合使用時,PropertyEditorRegistrar 實例尤其方便,CustomEditorConfigurer 暴露了一個名為 setPropertyEditorRegistrars(..) 的屬性。以這種方式添加到 CustomEditorConfigurerPropertyEditorRegistrar 實例可以輕鬆地與 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 框架 的人來說,結合資料綁定 web 控制器使用 PropertyEditorRegistrar 可能非常方便。以下範例在 @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 註冊程式碼封裝在一個類別中,然後在需要的眾多控制器之間共享。