方法注入

在大多數應用場景中,容器中的大部分 bean 都是單例。當一個單例 bean 需要與另一個單例 bean 協作,或者一個非單例 bean 需要與另一個非單例 bean 協作時,通常透過將一個 bean 定義為另一個 bean 的屬性來處理依賴關係。當 bean 的生命週期不同時,就會出現問題。假設單例 bean A 需要使用非單例(原型)bean B,可能在每次呼叫 A 的方法時都需要。容器只建立一次單例 bean A,因此只有一次機會設定屬性。容器無法在每次需要時為 bean A 提供 bean B 的新例項。

一個解決方案是放棄一些控制反轉。你可以透過實現 ApplicationContextAware 介面讓 bean A 知道容器,並透過每次 bean A 需要時呼叫 getBean("B") 向容器請求一個(通常是新的)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 框架。方法注入是 Spring IoC 容器的一個稍高階的特性,它能讓你乾淨地處理這個用例。

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

查詢方法注入

查詢方法注入是容器覆蓋容器管理 bean 上的方法並返回容器中另一個命名 bean 的查詢結果的能力。查詢通常涉及原型 bean,如上一節中描述的場景。Spring 框架透過使用 CGLIB 庫的位元組碼生成來動態生成覆蓋該方法的子類來實現此方法注入。

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

  • 對一個具有 abstract 方法的類進行單元測試,需要你自行建立該類的子類並提供 abstract 方法的樁實現。

  • 另一個關鍵限制是查詢方法不適用於工廠方法,尤其不適用於配置類中的 @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
}

訪問不同作用域目標 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

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

© . This site is unofficial and not affiliated with VMware.