Spring のアドバイス API

これで、Spring AOP がアドバイスを処理する方法を調べることができます。

アドバイスのライフサイクル

各アドバイスは Spring Bean です。アドバイスインスタンスは、すべてのアドバイスオブジェクトで共有することも、アドバイスオブジェクトごとに一意にすることもできます。これは、クラスごとまたはインスタンスごとのアドバイスに対応しています。

クラスごとのアドバイスが最も頻繁に使用されます。トランザクションアドバイザーなどの一般的なアドバイスに適しています。これらは、プロキシされるオブジェクトの状態に依存したり、新しい状態を追加したりしません。それらは単にメソッドと引数に基づいて動作します。

インスタンスごとのアドバイスは、ミックスインをサポートするための導入に適しています。この場合、アドバイスはプロキシされるオブジェクトに状態を追加します。

同じ AOP プロキシで共有アドバイスとインスタンスごとのアドバイスを組み合わせて使用できます。

Spring のアドバイス型

Spring はいくつかのアドバイス型を提供し、任意のアドバイス型をサポートするために拡張可能です。このセクションでは、基本概念と標準アドバイス型について説明します。

インターセプト Around アドバイス

Spring で最も基本的なアドバイス型は、アドバイスをインターセプトすることです。

Spring は、メソッドインターセプトを使用するアラウンドアドバイス用の AOP Alliance インターフェースに準拠しています。MethodInterceptor を実装し、アドバイスを実装するクラスは、次のインターフェースも実装する必要があります。

public interface MethodInterceptor extends Interceptor {

	Object invoke(MethodInvocation invocation) throws Throwable;
}

invoke() メソッドへの MethodInvocation 引数は、呼び出されるメソッド、ターゲットジョインポイント、AOP プロキシ、メソッドへの引数を公開します。invoke() メソッドは、呼び出しの結果、つまりジョインポイントの戻り値を返す必要があります。

次の例は、簡単な MethodInterceptor 実装を示しています。

  • Java

  • Kotlin

public class DebugInterceptor implements MethodInterceptor {

	public Object invoke(MethodInvocation invocation) throws Throwable {
		System.out.println("Before: invocation=[" + invocation + "]");
		Object rval = invocation.proceed();
		System.out.println("Invocation returned");
		return rval;
	}
}
class DebugInterceptor : MethodInterceptor {

	override fun invoke(invocation: MethodInvocation): Any {
		println("Before: invocation=[$invocation]")
		val rval = invocation.proceed()
		println("Invocation returned")
		return rval
	}
}

MethodInvocation の proceed() メソッドの呼び出しに注意してください。これはインターセプターチェーンを下ってジョインポイントに向かって進みます。ほとんどのインターセプターはこのメソッドを呼び出し、その戻り値を返します。ただし、MethodInterceptor は、他のアラウンドアドバイスと同様に、proceed メソッドを呼び出すのではなく、異なる値を返すか、例外をスローできます。ただし、正当な理由がない限り、これを行いたくありません。

MethodInterceptor 実装は、他の AOP Alliance 準拠の AOP 実装との相互運用性を提供します。このセクションの残りの部分で説明する他のアドバイス型は、一般的な AOP の概念を実装していますが、Spring 固有の方法です。最も具体的なアドバイス型を使用することには利点がありますが、別の AOP フレームワークでアスペクトを実行する可能性がある場合は、MethodInterceptor をアドバイスに使用してください。ポイントカットは現在フレームワーク間で相互運用可能ではなく、AOP Alliance は現在ポイントカットインターフェースを定義していないことに注意してください。

Before アドバイス

より単純なアドバイス型は、事前アドバイスです。これは、メソッドに入る前にのみ呼び出されるため、MethodInvocation オブジェクトは必要ありません。

before アドバイスの主な利点は、proceed() メソッドを呼び出す必要がないことです。インターセプターチェーンを不注意に進めることができない機能があります。

次のリストは、MethodBeforeAdvice インターフェースを示しています。

public interface MethodBeforeAdvice extends BeforeAdvice {

	void before(Method m, Object[] args, Object target) throws Throwable;
}

(Spring の API 設計では、アドバイスの前にフィールドを許可しますが、通常のオブジェクトはフィールドインターセプトに適用され、Spring がそれを実装することはほとんどありません)

戻り値の型は void であることに注意してください。Before アドバイスは、ジョインポイントの実行前にカスタム動作を挿入できますが、戻り値を変更することはできません。before アドバイスが例外をスローすると、インターセプターチェーンのそれ以上の実行を停止します。例外はインターセプターチェーンに伝搬します。チェックされていない場合、または呼び出されたメソッドの署名上にある場合は、直接クライアントに渡されます。それ以外の場合は、AOP プロキシによって未チェックの例外にラップされます。

次の例は、すべてのメソッド呼び出しをカウントする Spring の事前アドバイスを示しています。

  • Java

  • Kotlin

public class CountingBeforeAdvice implements MethodBeforeAdvice {

	private int count;

	public void before(Method m, Object[] args, Object target) throws Throwable {
		++count;
	}

	public int getCount() {
		return count;
	}
}
class CountingBeforeAdvice : MethodBeforeAdvice {

	var count: Int = 0

	override fun before(m: Method, args: Array<Any>, target: Any?) {
		++count
	}
}
Before アドバイスは、任意のポイントカットで使用できます。

Throws アドバイス

Throws アドバイスは、ジョインポイントが例外をスローした場合、ジョインポイントの復帰後に呼び出されます。Spring は、型付きスローのアドバイスを提供します。これは、org.springframework.aop.ThrowsAdvice インターフェースにメソッドが含まれていないことを意味することに注意してください。これは、指定されたオブジェクトが 1 つ以上の型付きスローアドバイスメソッドを実装することを識別するタグインターフェースです。これらは次の形式である必要があります。

afterThrowing([Method, args, target], subclassOfThrowable)

最後の引数のみが必要です。メソッドシグネチャーには、アドバイスメソッドがメソッドと引数に関心があるかどうかに応じて、1 つまたは 4 つの引数があります。次の 2 つのリストは、スローアドバイスの例であるクラスを示しています。

RemoteException がスローされた場合(サブクラスからも含む)、次のアドバイスが呼び出されます。

  • Java

  • Kotlin

public class RemoteThrowsAdvice implements ThrowsAdvice {

	public void afterThrowing(RemoteException ex) throws Throwable {
		// Do something with remote exception
	}
}
class RemoteThrowsAdvice : ThrowsAdvice {

	fun afterThrowing(ex: RemoteException) {
		// Do something with remote exception
	}
}

前のアドバイスとは異なり、次の例では 4 つの引数を宣言しているため、呼び出されたメソッド、メソッド引数、ターゲットオブジェクトにアクセスできます。ServletException がスローされると、次のアドバイスが呼び出されます。

  • Java

  • Kotlin

public class ServletThrowsAdviceWithArguments implements ThrowsAdvice {

	public void afterThrowing(Method m, Object[] args, Object target, ServletException ex) {
		// Do something with all arguments
	}
}
class ServletThrowsAdviceWithArguments : ThrowsAdvice {

	fun afterThrowing(m: Method, args: Array<Any>, target: Any, ex: ServletException) {
		// Do something with all arguments
	}
}

最後の例は、RemoteException と ServletException の両方を処理する単一のクラスでこれら 2 つのメソッドを使用する方法を示しています。1 つのクラスに任意の数の throws advice メソッドを組み合わせることができます。次のリストは、最後の例を示しています。

  • Java

  • Kotlin

public static class CombinedThrowsAdvice implements ThrowsAdvice {

	public void afterThrowing(RemoteException ex) throws Throwable {
		// Do something with remote exception
	}

	public void afterThrowing(Method m, Object[] args, Object target, ServletException ex) {
		// Do something with all arguments
	}
}
class CombinedThrowsAdvice : ThrowsAdvice {

	fun afterThrowing(ex: RemoteException) {
		// Do something with remote exception
	}

	fun afterThrowing(m: Method, args: Array<Any>, target: Any, ex: ServletException) {
		// Do something with all arguments
	}
}
throws-advice メソッドが例外自体をスローする場合、元の例外をオーバーライドします(つまり、ユーザーにスローされる例外を変更します)。オーバーライドの例外は通常、RuntimeException であり、メソッドシグネチャーと互換性があります。ただし、throws-advice メソッドがチェック済み例外をスローする場合は、ターゲットメソッドの宣言された例外と一致する必要があるため、特定のターゲットメソッドシグネチャーとある程度結びついています。ターゲットメソッドのシグネチャーと互換性のない、宣言されていないチェック例外をスローしないでください!
Throws アドバイスは、任意のポイントカットで使用できます。

After Returning アドバイス

Spring の after returning アドバイスは、次のリストに示す org.springframework.aop.AfterReturningAdvice インターフェースを実装する必要があります。

public interface AfterReturningAdvice extends Advice {

	void afterReturning(Object returnValue, Method m, Object[] args, Object target)
			throws Throwable;
}

after returning アドバイスは、戻り値(変更できない)、呼び出されたメソッド、メソッドの引数、ターゲットにアクセスできます。

次の after returning アドバイスは、例外をスローしていないすべての成功したメソッド呼び出しをカウントします。

  • Java

  • Kotlin

public class CountingAfterReturningAdvice implements AfterReturningAdvice {

	private int count;

	public void afterReturning(Object returnValue, Method m, Object[] args, Object target)
			throws Throwable {
		++count;
	}

	public int getCount() {
		return count;
	}
}
class CountingAfterReturningAdvice : AfterReturningAdvice {

	var count: Int = 0
		private set

	override fun afterReturning(returnValue: Any?, m: Method, args: Array<Any>, target: Any?) {
		++count
	}
}

このアドバイスは実行パスを変更しません。例外をスローすると、戻り値の代わりにインターセプターチェーンがスローされます。

After returning アドバイスは、任意のポイントカットで使用できます。

導入アドバイス

Spring は、導入アドバイスを特別な種類のインターセプトアドバイスとして扱います。

はじめに、次のインターフェースを実装する IntroductionAdvisor および IntroductionInterceptor が必要です。

public interface IntroductionInterceptor extends MethodInterceptor {

	boolean implementsInterface(Class intf);
}

AOP Alliance MethodInterceptor インターフェースから継承された invoke() メソッドは、導入を実装する必要があります。つまり、呼び出されたメソッドが導入されたインターフェース上にある場合、導入インターセプターはメソッド呼び出しの処理を担当します。proceed() を呼び出すことはできません。

導入アドバイスは、メソッドではなくクラスレベルでのみ適用されるため、ポイントカットでは使用できません。導入アドバイスは、次の方法がある IntroductionAdvisor でのみ使用できます。

public interface IntroductionAdvisor extends Advisor, IntroductionInfo {

	ClassFilter getClassFilter();

	void validateInterfaces() throws IllegalArgumentException;
}

public interface IntroductionInfo {

	Class<?>[] getInterfaces();
}

MethodMatcher はなく、導入アドバイスに関連付けられた Pointcut はありません。クラスフィルタリングのみが論理的です。

getInterfaces() メソッドは、このアドバイザーによって導入されたインターフェースを返します。

validateInterfaces() メソッドは、構成された IntroductionInterceptor によって導入されたインターフェースを実装できるかどうかを確認するために内部的に使用されます。

Spring Test スイートの例を検討し、1 つまたは複数のオブジェクトに次のインターフェースを導入するとします。

  • Java

  • Kotlin

public interface Lockable {
	void lock();
	void unlock();
	boolean locked();
}
interface Lockable {
	fun lock()
	fun unlock()
	fun locked(): Boolean
}

これはミックスインを示しています。アドバイスされたオブジェクトを Lockable にキャストできるようにしたいため、その型が何であっても、ロックおよびロック解除メソッドを呼び出します。lock() メソッドを呼び出す場合、すべての setter メソッドが LockedException をスローするようにします。オブジェクトを知らなくても不変にする機能を提供するアスペクトを追加できます: AOP の良い例です。

まず、重量物を持ち上げる IntroductionInterceptor が必要です。この場合、org.springframework.aop.support.DelegatingIntroductionInterceptor コンビニエンスクラスを継承します。IntroductionInterceptor を直接実装できますが、ほとんどの場合、DelegatingIntroductionInterceptor を使用するのが最適です。

DelegatingIntroductionInterceptor は、導入されたインターフェースの実際の実装への導入を委譲するように設計されており、そうするためにインターセプトの使用を隠しています。コンストラクター引数を使用して、任意のオブジェクトにデリゲートを設定できます。デフォルトのデリゲート(引数なしのコンストラクターが使用される場合)は this です。次の例では、デリゲートは DelegatingIntroductionInterceptor の LockMixin サブクラスです。デリゲート(デフォルトでは、それ自体)が与えられると、DelegatingIntroductionInterceptor インスタンスは、デリゲート(IntroductionInterceptor 以外)によって実装されるすべてのインターフェースを探し、それらのいずれかに対する導入をサポートします。LockMixin などのサブクラスは、suppressInterface(Class intf) メソッドを呼び出して、公開すべきでないインターフェースを抑制することができます。ただし、IntroductionInterceptor がサポートするインターフェースの数に関係なく、使用される IntroductionAdvisor は実際に公開されるインターフェースを制御します。導入されたインターフェースは、ターゲットによる同じインターフェースの実装を隠します。

LockMixin は DelegatingIntroductionInterceptor を継承し、Lockable 自体を実装します。スーパークラスは、Lockable を導入用にサポートできることを自動的に選択するため、指定する必要はありません。この方法で任意の数のインターフェースを導入できます。

locked インスタンス変数の使用に注意してください。これにより、ターゲットオブジェクトに保持されている状態に追加の状態が効果的に追加されます。

次の例は、LockMixin クラスの例を示しています。

  • Java

  • Kotlin

public class LockMixin extends DelegatingIntroductionInterceptor implements Lockable {

	private boolean locked;

	public void lock() {
		this.locked = true;
	}

	public void unlock() {
		this.locked = false;
	}

	public boolean locked() {
		return this.locked;
	}

	public Object invoke(MethodInvocation invocation) throws Throwable {
		if (locked() && invocation.getMethod().getName().indexOf("set") == 0) {
			throw new LockedException();
		}
		return super.invoke(invocation);
	}

}
class LockMixin : DelegatingIntroductionInterceptor(), Lockable {

	private var locked: Boolean = false

	fun lock() {
		this.locked = true
	}

	fun unlock() {
		this.locked = false
	}

	fun locked(): Boolean {
		return this.locked
	}

	override fun invoke(invocation: MethodInvocation): Any? {
		if (locked() && invocation.method.name.indexOf("set") == 0) {
			throw LockedException()
		}
		return super.invoke(invocation)
	}

}

多くの場合、invoke() メソッドをオーバーライドする必要はありません。通常、DelegatingIntroductionInterceptor 実装(メソッドが導入された場合は delegate メソッドを呼び出し、それ以外の場合はジョインポイントに向かって進みます)で十分です。この場合、チェックを追加する必要があります。ロックモードの場合、setter メソッドを呼び出すことはできません。

必要な導入部は、別個の LockMixin インスタンスを保持し、導入されたインターフェースを指定するだけです(この場合は Lockable のみ)。より複雑な例では、導入インターセプター(プロトタイプとして定義される)への参照を使用できます。この場合、LockMixin に関連する構成はないため、new を使用して構成を作成します。次の例は、LockMixinAdvisor クラスを示しています。

  • Java

  • Kotlin

public class LockMixinAdvisor extends DefaultIntroductionAdvisor {

	public LockMixinAdvisor() {
		super(new LockMixin(), Lockable.class);
	}
}
class LockMixinAdvisor : DefaultIntroductionAdvisor(LockMixin(), Lockable::class.java)

このアドバイザは、構成を必要としないため、非常に簡単に適用できます。(ただし、IntroductionAdvisor を使用せずに IntroductionInterceptor を使用することはできません)導入では通常どおり、アドバイザーはステートフルであるため、インスタンスごとである必要があります。アドバイスされたオブジェクトごとに LockMixinAdvisor の異なるインスタンス、LockMixin が必要です。アドバイザーは、アドバイスされたオブジェクトの状態の一部を構成します。

他のアドバイザと同様に、Advised.addAdvisor() メソッドを使用するか、XML 構成で(推奨される方法)を使用して、このアドバイザをプログラムで適用できます。「自動プロキシ作成者」を含む、以下で説明するすべてのプロキシ作成の選択は、導入とステートフルミックスインを正しく処理します。