方法注入

在大多數應用情境中,容器中的大多數 Bean 都是 單例。當單例 Bean 需要與另一個單例 Bean 協作,或非單例 Bean 需要與另一個非單例 Bean 協作時,您通常會將一個 Bean 定義為另一個 Bean 的屬性來處理相依性。當 Bean 生命週期不同時,就會出現問題。假設單例 Bean A 需要使用非單例 (原型) Bean B,可能在每次調用 A 的方法時都需要。容器只會建立單例 Bean A 一次,因此只有一次設定屬性的機會。容器無法在每次需要時都為 Bean A 提供 Bean B 的新實例。

一個解決方案是放棄一些控制反轉。您可以透過實作 ApplicationContextAware 介面讓 Bean A 知道容器,並透過呼叫容器的 getBean("B") 來要求(通常是新的)Bean B 實例,每次 Bean A 需要它時。以下範例展示了這種方法

  • Java

  • Kotlin

package fiona.apple;

// Spring-API imports
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;

/**
 * A class that uses a stateful Command-style class to perform
 * some processing.
 */
public class CommandManager implements ApplicationContextAware {

	private ApplicationContext applicationContext;

	public Object process(Map commandState) {
		// grab a new instance of the appropriate Command
		Command command = createCommand();
		// set the state on the (hopefully brand new) Command instance
		command.setState(commandState);
		return command.execute();
	}

	protected Command createCommand() {
		// notice the Spring API dependency!
		return this.applicationContext.getBean("command", Command.class);
	}

	public void setApplicationContext(
			ApplicationContext applicationContext) throws BeansException {
		this.applicationContext = applicationContext;
	}
}
package fiona.apple

// Spring-API imports
import org.springframework.context.ApplicationContext
import org.springframework.context.ApplicationContextAware

// A class that uses a stateful Command-style class to perform
// some processing.
class CommandManager : ApplicationContextAware {

	private lateinit var applicationContext: ApplicationContext

	fun process(commandState: Map<*, *>): Any {
		// grab a new instance of the appropriate Command
		val command = createCommand()
		// set the state on the (hopefully brand new) Command instance
		command.state = commandState
		return command.execute()
	}

	// notice the Spring API dependency!
	protected fun createCommand() =
			applicationContext.getBean("command", Command::class.java)

	override fun setApplicationContext(applicationContext: ApplicationContext) {
		this.applicationContext = applicationContext
	}
}

上述方法並不理想,因為業務程式碼知道 Spring Framework 並與之耦合。方法注入是 Spring IoC 容器的一個稍微進階的功能,可讓您乾淨俐落地處理這種用例。

您可以在 這篇部落格文章中閱讀更多關於方法注入動機的資訊。

查找方法注入

查找方法注入是容器覆寫容器管理 Bean 的方法,並傳回容器中另一個具名 Bean 的查找結果的能力。查找通常涉及原型 Bean,如前一節中所述的情境。Spring Framework 透過使用 CGLIB 函式庫的位元組碼產生來動態產生一個子類別來實作此方法注入,該子類別會覆寫該方法。

  • 為了使這種動態子類別化能夠運作,Spring Bean 容器子類別化的類別不能是 final,並且要覆寫的方法也不能是 final

  • 單元測試具有 abstract 方法的類別需要您自行子類別化該類別,並提供 abstract 方法的 Stub 實作。

  • 具體方法對於組件掃描也是必要的,組件掃描需要具體類別才能拾取。

  • 另一個關鍵限制是查找方法不適用於 Factory 方法,尤其不適用於組態類別中的 @Bean 方法,因為在這種情況下,容器不負責建立實例,因此無法動態即時建立子類別。

在先前程式碼片段中的 CommandManager 類別的情況下,Spring 容器會動態覆寫 createCommand() 方法的實作。CommandManager 類別沒有任何 Spring 相依性,如下面的修改範例所示

  • Java

  • Kotlin

package fiona.apple;

// no more Spring imports!

public abstract class CommandManager {

	public Object process(Object commandState) {
		// grab a new instance of the appropriate Command interface
		Command command = createCommand();
		// set the state on the (hopefully brand new) Command instance
		command.setState(commandState);
		return command.execute();
	}

	// okay... but where is the implementation of this method?
	protected abstract Command createCommand();
}
package fiona.apple

// no more Spring imports!

abstract class CommandManager {

	fun process(commandState: Any): Any {
		// grab a new instance of the appropriate Command interface
		val command = createCommand()
		// set the state on the (hopefully brand new) Command instance
		command.state = commandState
		return command.execute()
	}

	// okay... but where is the implementation of this method?
	protected abstract fun createCommand(): Command
}

在包含要注入的方法的用戶端類別(在本例中為 CommandManager)中,要注入的方法需要以下形式的簽章

<public|protected> [abstract] <return-type> theMethodName(no-arguments);

如果方法是 abstract,則動態產生的子類別會實作該方法。否則,動態產生的子類別會覆寫原始類別中定義的具體方法。請考慮以下範例

<!-- a stateful bean deployed as a prototype (non-singleton) -->
<bean id="myCommand" class="fiona.apple.AsyncCommand" scope="prototype">
	<!-- inject dependencies here as required -->
</bean>

<!-- commandManager uses myCommand prototype bean -->
<bean id="commandManager" class="fiona.apple.CommandManager">
	<lookup-method name="createCommand" bean="myCommand"/>
</bean>

識別為 commandManager 的 Bean 在每次需要 myCommand Bean 的新實例時,都會呼叫自己的 createCommand() 方法。如果實際需要原型,您必須小心將 myCommand Bean 部署為原型。如果它是單例,則每次都會傳回 myCommand Bean 的相同實例。

或者,在基於註解的組件模型中,您可以透過 @Lookup 註解宣告查找方法,如下列範例所示

  • Java

  • Kotlin

public abstract class CommandManager {

	public Object process(Object commandState) {
		Command command = createCommand();
		command.setState(commandState);
		return command.execute();
	}

	@Lookup("myCommand")
	protected abstract Command createCommand();
}
abstract class CommandManager {

	fun process(commandState: Any): Any {
		val command = createCommand()
		command.state = commandState
		return command.execute()
	}

	@Lookup("myCommand")
	protected abstract fun createCommand(): Command
}

或者,更慣用地,您可以依靠目標 Bean 根據宣告的查找方法傳回類型來解析

  • Java

  • Kotlin

public abstract class CommandManager {

	public Object process(Object commandState) {
		Command command = createCommand();
		command.setState(commandState);
		return command.execute();
	}

	@Lookup
	protected abstract Command createCommand();
}
abstract class CommandManager {

	fun process(commandState: Any): Any {
		val command = createCommand()
		command.state = commandState
		return command.execute()
	}

	@Lookup
	protected abstract fun createCommand(): Command
}

請注意,您通常應該使用具體的 Stub 實作來宣告此類註解的查找方法,以便它們與 Spring 的組件掃描規則相容,其中抽象類別預設會被忽略。此限制不適用於明確註冊或明確匯入的 Bean 類別。

存取不同作用域目標 Bean 的另一種方法是 ObjectFactory/Provider 注入點。請參閱作為相依性的作用域 Bean

您也可能會發現 ServiceLocatorFactoryBean(在 org.springframework.beans.factory.config 套件中)很有用。

任意方法取代

與查找方法注入相比,一種較不常用的方法注入形式是將受管 Bean 中的任意方法替換為另一種方法實作的能力。您可以安全地跳過本節的其餘部分,直到您實際需要此功能為止。

使用基於 XML 的組態元資料,您可以使用 replaced-method 元素將已部署 Bean 的現有方法實作替換為另一個實作。請考慮以下類別,它有一個名為 computeValue 的方法,我們想要覆寫它

  • Java

  • Kotlin

public class MyValueCalculator {

	public String computeValue(String input) {
		// some real code...
	}

	// some other methods...
}
class MyValueCalculator {

	fun computeValue(input: String): String {
		// some real code...
	}

	// some other methods...
}

實作 org.springframework.beans.factory.support.MethodReplacer 介面的類別提供了新的方法定義,如下列範例所示

  • Java

  • Kotlin

/**
 * meant to be used to override the existing computeValue(String)
 * implementation in MyValueCalculator
 */
public class ReplacementComputeValue implements MethodReplacer {

	public Object reimplement(Object o, Method m, Object[] args) throws Throwable {
		// get the input value, work with it, and return a computed result
		String input = (String) args[0];
		...
		return ...;
	}
}
/**
 * meant to be used to override the existing computeValue(String)
 * implementation in MyValueCalculator
 */
class ReplacementComputeValue : MethodReplacer {

	override fun reimplement(obj: Any, method: Method, args: Array<out Any>): Any {
		// get the input value, work with it, and return a computed result
		val input = args[0] as String;
		...
		return ...;
	}
}

部署原始類別並指定方法覆寫的 Bean 定義將類似於以下範例

<bean id="myValueCalculator" class="x.y.z.MyValueCalculator">
	<!-- arbitrary method replacement -->
	<replaced-method name="computeValue" replacer="replacementComputeValue">
		<arg-type>String</arg-type>
	</replaced-method>
</bean>

<bean id="replacementComputeValue" class="a.b.c.ReplacementComputeValue"/>

您可以在 <replaced-method/> 元素中使用一個或多個 <arg-type/> 元素來指示要覆寫的方法的方法簽章。只有當方法被多載並且類別中存在多個變體時,才需要引數的簽章。為了方便起見,引數的類型字串可以是完整限定類型名稱的子字串。例如,以下所有項目都符合 java.lang.String

java.lang.String
String
Str

由於引數的數量通常足以區分每個可能的選擇,因此此捷徑可以透過讓您僅輸入與引數類型匹配的最短字串來節省大量輸入。