メソッドインジェクション

ほとんどのアプリケーションシナリオでは、コンテナー内のほとんどの Bean はシングルトンです。シングルトン Bean が別のシングルトン Bean と連携する必要がある場合、または非シングルトン Bean が別の非シングルトン Bean と連携する必要がある場合、通常は、一方の Bean をもう一方の Bean のプロパティとして定義することで依存関係を処理します。Bean のライフサイクルが異なる場合、問題が発生します。シングルトン Bean A が、おそらく A のメソッド呼び出しごとに、非シングルトン (プロトタイプ) Bean B を使用する必要があるとします。コンテナーはシングルトン Bean A を 1 回だけ作成するため、プロパティを設定する機会は 1 回だけです。コンテナーは、Bean A に Bean B の新しいインスタンスが必要になるたびに提供することはできません。

解決策は、制御の逆転を回避することです。ApplicationContextAware インターフェースを実装し、Bean A が必要とするたびに (通常は新しい) Bean B インスタンスを要求するコンテナーへの getBean("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 メソッドのスタブ実装を提供する必要があります。

  • 具象メソッドは、コンポーネントのスキャンにも必要です。これには、具象クラスを取得する必要があります。

  • さらに重要な制限は、ルックアップメソッドがファクトリメソッドでは機能せず、特に構成クラスの @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
}

通常、抽象クラスがデフォルトで無視される Spring のコンポーネントスキャンルールと互換性を持たせるために、具体的なスタブ実装でこのようなアノテーション付きルックアップメソッドを宣言する必要があります。この制限は、明示的に登録または明示的にインポートされた Bean クラスには適用されません。

スコープの異なるターゲット Bean にアクセスする別の方法は、ObjectFactoryProvider インジェクションポイントです。依存関係としてのスコープ 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/> 要素内で 1 つ以上の <arg-type/> 要素を使用して、オーバーライドされるメソッドのメソッドシグネチャーを示すことができます。引数の署名は、メソッドがオーバーロードされ、クラス内に複数のバリアントが存在する場合にのみ必要です。便宜上、引数の型文字列は完全修飾型名の部分文字列である場合があります。例: 以下はすべて java.lang.String に一致します:

java.lang.String
String
Str

多くの場合、引数の数はそれぞれの可能な選択肢を区別するのに十分なので、引数型に一致する最短の文字列のみを入力できるようにすることで、このショートカットは多くの入力を節約できます。