資料繫結

資料繫結對於將使用者輸入繫結到目標物件非常有用,其中使用者輸入是一個以屬性路徑為鍵的對映,遵循 JavaBeans 約定DataBinder 是支援此功能的主要類,它提供了兩種繫結使用者輸入的方法:

  • 建構函式繫結 - 將使用者輸入繫結到公共資料建構函式,並在使用者輸入中查詢建構函式引數值。

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

你可以同時應用建構函式繫結和屬性繫結,或者只應用其中一種。

建構函式繫結

要使用建構函式繫結:

  1. 建立一個目標物件為 nullDataBinder

  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

Spring 使用 PropertyEditor 的概念來實現 ObjectString 之間的轉換。以與物件本身不同的方式表示屬性會很方便。例如,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 實現:

表 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

將 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())
	}
}

最後,以下示例展示瞭如何使用 CustomEditorConfigurerApplicationContext 註冊新的 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(..) 的屬性。以這種方式新增到 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 框架 的使用者,將 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 註冊程式碼封裝在一個類中,然後在需要時在多個控制器之間共享。