方法注入

在大多數應用場景中,容器中的大多數 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 A 每次需要時獲取(通常是新的)Bean B 例項。以下示例展示了這種方法

  • 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 容器的一個稍微高階的特性,它允許你乾淨利落地處理此用例。

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

Lookup 方法注入

Lookup 方法注入是容器覆蓋容器管理的 Bean 的方法並返回容器中另一個指定名稱的 Bean 的查詢結果的能力。查詢通常涉及原型 Bean,就像前面部分描述的場景那樣。Spring Framework 透過使用 CGLIB 庫的位元組碼生成來動態生成一個覆蓋該方法的子類來實現這種方法注入。

  • 為了使這種動態子類化工作,Spring Bean 容器子類化的類不能是 final 的,並且要被覆蓋的方法也不能是 final 的。

  • 單元測試一個包含 abstract 方法的類需要你自己子類化該類並提供抽象方法的 stub 實現。

  • 另一個關鍵限制是 lookup 方法不適用於工廠方法,特別是配置類中的 @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 部署為原型(prototype)。如果它是單例(singleton),則每次返回的都是 myCommand Bean 的同一個例項。

或者,在基於註解的元件模型中,可以透過 @Lookup 註解宣告一個 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
}

或者,更慣用的做法是,你可以依賴於根據 lookup 方法宣告的返回型別解析目標 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
}

訪問不同作用域目標 Bean 的另一種方法是 ObjectFactory/Provider 注入點。請參閱作用域 Bean 作為依賴項

你可能還會發現 ServiceLocatorFactoryBean(位於 org.springframework.beans.factory.config 包中)也很有用。

任意方法替換

方法注入中一種不如 lookup 方法注入有用的形式是,用另一個方法實現替換託管 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

由於引數數量通常足以區分各種可能的選擇,此快捷方式可以節省大量輸入,只需鍵入與引數型別匹配的最短字串即可。