プロキシメカニズム

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

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

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

  • final クラスは拡張できないため、プロキシできません。

  • final メソッドはオーバーライドできないため、アドバイスできません。

  • private メソッドはオーバーライドできないため、アドバイスできません。

  • 表示されていないメソッド (たとえば、別のパッケージの親クラスのパッケージプライベートメソッド) は、事実上プライベートであるため、アドバイスできません。

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

  • CGLIB プロキシの使用は、Java モジュールシステムによって制限される可能性があります。一般的なケースとして、モジュールパスにデプロイする場合、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 参照に対して呼び出されます。これには重要な意味があります。つまり、自己呼び出しでは、メソッド呼び出しに関連付けられたアドバイスが実行されないということです。言い換えると、明示的または暗黙的な this 参照による自己呼び出しでは、アドバイスがバイパスされます。

これを解決するには、次のオプションがあります。

自己呼び出しを避ける

最善のアプローチ (ここでは「最善」という用語はあいまいに使用されています) は、自己呼び出しが発生しないようにコードをリファクタリングすることです。これには多少の作業が必要ですが、最も影響の少ない最善のアプローチです。

自己参照を挿入する

別の方法としては、自己注入を利用し、this ではなく自己参照を介してプロキシ上のメソッドを呼び出すことです。

AopContext.currentProxy() を使用する

この最後のアプローチは強く推奨されておらず、以前のオプションを優先してこれを指摘するのはためらわれます。ただし、最後の手段として、次の例に示すように、クラス内のロジックを Spring AOP に結び付けることもできます。

  • Java

  • Kotlin

public class SimplePojo implements Pojo {

	public void foo() {
		// This works, but it should be avoided if possible.
		((Pojo) AopContext.currentProxy()).bar();
	}

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

	fun foo() {
		// This works, but it should be avoided if possible.
		(AopContext.currentProxy() as Pojo).bar()
	}

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

AopContext.currentProxy() を使用すると、コードが Spring AOP に完全に結合され、クラス自体が AOP コンテキストで使用されていることを認識するようになり、AOP の利点の一部が損なわれます。また、次の例に示すように、プロキシを公開するように ProxyFactory を構成する必要があります。

  • 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 のコンパイル時ウィービングとロード時ウィービングでは、プロキシ経由ではなくバイトコード内でアドバイスを適用するため、この自己呼び出しの課題は発生しません。