プロキシメカニズム
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 プロキシについて
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...
}
}
オブジェクト参照でメソッドを呼び出すと、次のイメージとリストに示すように、メソッドはそのオブジェクト参照で直接呼び出されます。
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()
}
クライアントコードの参照がプロキシの場合、状況はわずかに変わります。次の図とコードスニペットを検討してください。
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 のコンパイル時ウィービングとロード時ウィービングでは、プロキシ経由ではなくバイトコード内でアドバイスを適用するため、この自己呼び出しの課題は発生しません。 |