プロキシメカニズム

Spring AOP は、JDK 動的プロキシまたは CGLIB を使用して、指定されたターゲットオブジェクトのプロキシを作成します。JDK 動的プロキシは JDK に組み込まれていますが、CGLIB は一般的なオープンソースクラス定義ライブラリです(spring-core に再パッケージ化されています)。

プロキシされるターゲットオブジェクトが少なくとも 1 つのインターフェースを実装する場合、JDK 動的プロキシが使用されます。ターゲット型によって実装されるすべてのインターフェースがプロキシされます。ターゲットオブジェクトがインターフェースを実装しない場合、CGLIB プロキシが作成されます。

CGLIB プロキシの使用を強制する場合(たとえば、インターフェースによって実装されているメソッドだけでなく、ターゲットオブジェクトに対して定義されているすべてのメソッドをプロキシする)、そうすることができます。ただし、次の課題を考慮する必要があります。

  • CGLIB では、final メソッドはランタイム生成サブクラスでオーバーライドできないため、アドバイスできません。

  • Spring 4.0 以降、CGLIB プロキシインスタンスは Objenesis を介して作成されるため、プロキシオブジェクトのコンストラクターが 2 回呼び出されることはなくなりました。JVM がコンストラクターのバイパスを許可しない場合にのみ、Spring の AOP サポートからの二重呼び出しと対応するデバッグログエントリが表示されることがあります。

  • CGLIB プロキシの使用は、JDK 9+ プラットフォームモジュールシステムで制限を受ける場合があります。一般的なケースとして、モジュールパスにデプロイする場合、java.lang パッケージのクラスの CGLIB プロキシを作成することはできません。このようなケースでは、モジュールで使用できない JVM ブートストラップフラグ --add-opens=java.base/java.lang=ALL-UNNAMED が必要です。

CGLIB プロキシの使用を強制するには、次のように、<aop:config> 要素の proxy-target-class 属性の値を true に設定します。

<aop:config proxy-target-class="true">
	<!-- other beans defined here... -->
</aop:config>

@AspectJ 自動プロキシサポートを使用するときに CGLIB プロキシを強制するには、次のように <aop:aspectj-autoproxy> 要素の proxy-target-class 属性を true に設定します。

<aop:aspectj-autoproxy proxy-target-class="true"/>

複数の <aop:config/> セクションは、実行時に単一の統合された自動プロキシクリエーターにまとめられ、<aop:config/> セクションのいずれか(通常は異なる XML Bean 定義ファイルから)が指定した最も強力なプロキシ設定が適用されます。これは、<tx:annotation-driven/> および <aop:aspectj-autoproxy/> 要素にも適用されます。

明確にするために、<tx:annotation-driven/><aop:aspectj-autoproxy/><aop:config/> 要素で proxy-target-class="true" を使用すると、3 つすべてに CGLIB プロキシが強制的に使用されます

AOP プロキシについて

Spring AOP はプロキシベースです。独自のアスペクトを記述したり、Spring Framework で提供される Spring AOP ベースのアスペクトを使用する前に、最後のステートメントが実際に意味するセマンティクスを把握することが非常に重要です。

次のコードスニペットが示すように、まず、プレーンバニラ、プロキシ化されていない、特別なものは何もない、ストレートオブジェクト参照があるシナリオを考えます。

  • Java

  • Kotlin

public class SimplePojo implements Pojo {

	public void foo() {
		// this next method invocation is a direct call on the 'this' reference
		this.bar();
	}

	public void bar() {
		// some logic...
	}
}
class SimplePojo : Pojo {

	fun foo() {
		// this next method invocation is a direct call on the 'this' reference
		this.bar()
	}

	fun bar() {
		// some logic...
	}
}

オブジェクト参照でメソッドを呼び出すと、次のイメージとリストに示すように、メソッドはそのオブジェクト参照で直接呼び出されます。

aop proxy plain pojo call
  • Java

  • Kotlin

public class Main {

	public static void main(String[] args) {
		Pojo pojo = new SimplePojo();
		// this is a direct method call on the 'pojo' reference
		pojo.foo();
	}
}
fun main() {
	val pojo = SimplePojo()
	// this is a direct method call on the 'pojo' reference
	pojo.foo()
}

クライアントコードの参照がプロキシの場合、状況はわずかに変わります。次の図とコードスニペットを検討してください。

aop proxy call
  • Java

  • Kotlin

public class Main {

	public static void main(String[] args) {
		ProxyFactory factory = new ProxyFactory(new SimplePojo());
		factory.addInterface(Pojo.class);
		factory.addAdvice(new RetryAdvice());

		Pojo pojo = (Pojo) factory.getProxy();
		// this is a method call on the proxy!
		pojo.foo();
	}
}
fun main() {
	val factory = ProxyFactory(SimplePojo())
	factory.addInterface(Pojo::class.java)
	factory.addAdvice(RetryAdvice())

	val pojo = factory.proxy as Pojo
	// this is a method call on the proxy!
	pojo.foo()
}

ここで理解しておくべき重要なことは、Main クラスの main(..) メソッド内のクライアントコードがプロキシへの参照を持っていることです。つまり、そのオブジェクト参照に対するメソッド呼び出しは、プロキシに対する呼び出しです。その結果、プロキシは、その特定のメソッド呼び出しに関連するすべてのインターセプター(アドバイス)に委譲できます。ただし、呼び出しが最終的にターゲットオブジェクト(この場合は SimplePojo 参照)に到達すると、this.bar() や this.foo() など、それ自体で行うメソッド呼び出しは、プロキシではなく this 参照に対して呼び出されます。これには重要な意味があります。つまり、自己呼び出しでは、メソッド呼び出しに関連するアドバイスが実行される可能性はありません。

では、これについてはどうすればいいのでしょうか? 最良の方法は (ここでは「最良」という言葉をゆるく使っています)、自己呼び出しが起こらないようにコードをリファクタリングすることです。これはあなたの多少の作業を必要としますが、これが最良であり、最も侵襲性の低いアプローチです。次のアプローチは非常に恐ろしいものですが、それを指摘することを躊躇しています。次の例が示すように、クラス内のロジックを Spring AOP に完全に結びつけることができます(私たちにとっては苦痛です)。

  • Java

  • Kotlin

public class SimplePojo implements Pojo {

	public void foo() {
		// this works, but... gah!
		((Pojo) AopContext.currentProxy()).bar();
	}

	public void bar() {
		// some logic...
	}
}
class SimplePojo : Pojo {

	fun foo() {
		// this works, but... gah!
		(AopContext.currentProxy() as Pojo).bar()
	}

	fun bar() {
		// some logic...
	}
}

これにより、コードが Spring AOP に完全に結合され、AOP に直面して飛行する AOP コンテキストで使用されているという事実がクラス自体に認識されます。また、次の例に示すように、プロキシを作成するときに追加の構成が必要です。

  • Java

  • Kotlin

public class Main {

	public static void main(String[] args) {
		ProxyFactory factory = new ProxyFactory(new SimplePojo());
		factory.addInterface(Pojo.class);
		factory.addAdvice(new RetryAdvice());
		factory.setExposeProxy(true);

		Pojo pojo = (Pojo) factory.getProxy();
		// this is a method call on the proxy!
		pojo.foo();
	}
}
fun main() {
	val factory = ProxyFactory(SimplePojo())
	factory.addInterface(Pojo::class.java)
	factory.addAdvice(RetryAdvice())
	factory.isExposeProxy = true

	val pojo = factory.proxy as Pojo
	// this is a method call on the proxy!
	pojo.foo()
}

最後に、AspectJ はプロキシベースの AOP フレームワークではないため、この自己呼び出しの課題はありません。