ちょっとしたヒントを始める前に:@Around("logging()")
ポイントカット メソッドを記述する場合は、loggingResponseTime()
実際logging()
に から に名前を変更する必要があります。そうしないと、アスペクトが機能しません。
ここで、実際の問題について説明します。コードを広範にアドバイスすることで、典型的な初心者の間違いを犯しています。つまり、(JDK の外部で)すべてのメソッド実行をインターセプトしています。Eclipse と AJDT を使用していて、tracing()
アドバイスにカーソルを置くと、現在のポイントカットを使用して AspectJ の「相互参照」ウィンドウに次のようなものが表示されます。
問題はすぐにわかります。ポイントカットは匿名ThreadLocal
サブクラスのコードをキャプチャします。これは無限の再帰につながり、最終的にはStackOverflowError
、自分のコールスタックを調べるとわかるようになります。
ここで、他の人の参照用に問題を示すサンプル コードをいくつか示します。
ドライバー アプリケーション:
package de.scrum_master.app;
public class Application {
public static void main(String[] args) {
System.out.println(bar(foo()));
}
public static String bar(String text) {
return text + "bar";
}
private static String foo() {
return "foo";
}
}
側面:
package de.scrum_master.aspect;
import java.util.UUID;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class DemoAspect {
private static ThreadLocal<String> id = new ThreadLocal<String>() {
@Override
protected String initialValue() {
return UUID.randomUUID().toString();
}
};
@Pointcut("execution(* *(..))")
public void logging() {}
@Around("logging()")
public Object tracing(ProceedingJoinPoint thisJoinPoint) throws Throwable {
String methodSignature = thisJoinPoint.getSignature().toString();
if (id.get().toString() == null || id.get().toString().length() == 0)
id.set(UUID.randomUUID().toString());
System.out.println("Entering into " + methodSignature);
Object ret = thisJoinPoint.proceed();
System.out.println(id.get().toString());
System.out.println("Exiting from " + methodSignature);
id.remove();
return ret;
}
}
コンソール出力:
Exception in thread "main" java.lang.StackOverflowError
at org.aspectj.runtime.reflect.SignatureImpl$CacheImpl.get(SignatureImpl.java:217)
at org.aspectj.runtime.reflect.SignatureImpl.toString(SignatureImpl.java:50)
at org.aspectj.runtime.reflect.SignatureImpl.toString(SignatureImpl.java:62)
at de.scrum_master.aspect.DemoAspect$1.initialValue_aroundBody1$advice(DemoAspect.aj:29)
at de.scrum_master.aspect.DemoAspect$1.initialValue(DemoAspect.aj:1)
at de.scrum_master.aspect.DemoAspect$1.initialValue(DemoAspect.aj:1)
at java.lang.ThreadLocal.setInitialValue(ThreadLocal.java:160)
at java.lang.ThreadLocal.get(ThreadLocal.java:150)
at de.scrum_master.aspect.DemoAspect$1.initialValue_aroundBody1$advice(DemoAspect.aj:30)
at de.scrum_master.aspect.DemoAspect$1.initialValue(DemoAspect.aj:1)
at de.scrum_master.aspect.DemoAspect$1.initialValue(DemoAspect.aj:1)
at java.lang.ThreadLocal.setInitialValue(ThreadLocal.java:160)
at java.lang.ThreadLocal.get(ThreadLocal.java:150)
(...)
それで、あなたは何ができますか?それは実際には非常に簡単です: インターセプトしたくないジョインポイントをポイントカットから除外するだけです。そのためには、いくつかのオプションがあります。私はいくつかの名前を挙げています:
A)アスペクトを特定のパッケージに入れ、そのパッケージ内のすべての (アスペクト) クラスを除外します。
@Pointcut("execution(* *(..)) && !within(de.scrum_master.aspect..*)")
B)によって注釈が付けられたすべてのクラスを除外します@Aspect
。
@Pointcut("execution(* *(..)) && !within(@org.aspectj.lang.annotation.Aspect *)")
C)次のような特定の命名スキームに一致するすべての (アスペクト) クラスを除外します*Aspect
。
@Pointcut("execution(* *(..)) && !within(*..*Aspect)")
D)ThreadLocal
すべてのサブクラスからコードを除外する(+
構文):
@Pointcut("execution(* *(..)) && !within(ThreadLocal+)")
いずれの場合も、結果は同じになります。
Entering into void de.scrum_master.app.Application.main(String[])
Entering into String de.scrum_master.app.Application.foo()
d2b83f5f-7282-4c06-9b81-6601c8e0499d
Exiting from String de.scrum_master.app.Application.foo()
Entering into String de.scrum_master.app.Application.bar(String)
0d1c9463-4bbd-427d-9d64-c7f3967756cf
Exiting from String de.scrum_master.app.Application.bar(String)
foobar
aa96bbbd-a1a1-450f-ae6e-77ab204c5fb2
Exiting from void de.scrum_master.app.Application.main(String[])
ところで、UUID
ここで高価なオブジェクトを作成する価値がないと思うので、私はあなたの s の使用法に強い疑問を持っています。タイムスタンプをログに記録するだけではどうですか? ログにグローバルに一意の ID が必要なのはなぜですか? 彼らはあなたに何も言わない。さらに、スレッドごとに 1 つの ID を作成するだけでなく、コメント化されていないものを使用すると、id.remove()
呼び出しごとに 1 つの ID を作成することになります! 申し訳ありませんが、これは肥大化しており、コードの速度が低下し、不要なオブジェクトが多数作成されます。これは賢明ではないと思います。
アップデート:
無限再帰の理由を説明するのを忘れていました。あなたのアドバイスはThreadLocal.get()
、null である可能性があると仮定して を呼び出します。get()
値が初期化されていない場合は、を利用して初期化するため、実際にはそうではありませんinitialValue()
。を手動で呼び出してもremove()
、次に呼び出すget()
と値が再度初期化されます。これは文書化された動作です:
このスレッドローカル変数の現在のスレッドのコピーの値を返します。変数に現在のスレッドの値がない場合は、まずinitialValue()
メソッドの呼び出しによって返される値に初期化されます。
では、一歩一歩、何が起こるでしょうか?
- メソッドが呼び出されます。
- あなたの周りのアドバイスが有効になります。
id.get()
あなたはアドバイスから電話します。
ThreadLocal.get()
値が設定されているかどうかを確認し、値がないことに気づき、オーバーライドされinitialValue()
たメソッドを呼び出します。
- オーバーライドさ
initialValue()
れたメソッドは match-all pointcut によってキャプチャされるため、初期値が設定される前にexecution(* *(..))
アドバイスが有効になります。最終結果は、ループが再び開始されるということです-無限の再帰、quod erat demostrandum。
get()
したがって、実際の問題は、アドバイスから初期化されていないThreadLocal
サブクラスを呼び出すと同時にinitialValue()
、同じアドバイスでユーザー定義のメソッドをターゲットにすることに要約されます。これが無限の再帰を作成し、最終的にスタック オーバーフローを引き起こします。
ポイントカットからアスペクトを除外することをお勧めします。上記のポイントカットの例を参照してください。また、値のnull
チェックは不要なので削除する必要があります。ThreadLocal
最後になりましたが、ThreadLocal
メソッド呼び出しごとではなく、スレッドごとに 1 つの値が必要であると仮定します。set()
したがって、またはremove()
コールをまったく使用せずに行うことができます。
ドライバー クラスを変更し、追加のスレッドを作成します。
package de.scrum_master.app;
public class Application {
public static void main(String[] args) throws InterruptedException {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(bar(foo()));
}
}).start();
Thread.sleep(200);
}
public static String bar(String text) {
return text + "bar";
}
private static String foo() {
return "foo";
}
}
改善された側面:
package de.scrum_master.aspect;
import java.util.UUID;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class DemoAspect {
private static ThreadLocal<UUID> id = new ThreadLocal<UUID>() {
@Override
protected UUID initialValue() {
return UUID.randomUUID();
}
};
@Pointcut("execution(* *(..)) && !within(DemoAspect)")
public void logging() {}
@Around("logging()")
public Object tracing(ProceedingJoinPoint thisJoinPoint) throws Throwable {
Signature methodSignature = thisJoinPoint.getSignature();
System.out.println(
"Thread " + Thread.currentThread().getId() +
"[" + id.get() +
"] >>> " + methodSignature
);
Object result = thisJoinPoint.proceed();
System.out.println(
"Thread " + Thread.currentThread().getId() +
"[" + id.get() +
"] <<< " + methodSignature
);
return result;
}
}
コンソール出力:
Thread 1[549d0856-0a92-4031-9331-a1317d6a43c4] >>> void de.scrum_master.app.Application.main(String[])
Thread 9[32c8444c-0f1f-4023-9b97-69d5beda3b4c] >>> void de.scrum_master.app.Application.1.run()
Thread 9[32c8444c-0f1f-4023-9b97-69d5beda3b4c] >>> String de.scrum_master.app.Application.access$0()
Thread 9[32c8444c-0f1f-4023-9b97-69d5beda3b4c] >>> String de.scrum_master.app.Application.foo()
Thread 9[32c8444c-0f1f-4023-9b97-69d5beda3b4c] <<< String de.scrum_master.app.Application.foo()
Thread 9[32c8444c-0f1f-4023-9b97-69d5beda3b4c] <<< String de.scrum_master.app.Application.access$0()
Thread 9[32c8444c-0f1f-4023-9b97-69d5beda3b4c] >>> String de.scrum_master.app.Application.bar(String)
Thread 9[32c8444c-0f1f-4023-9b97-69d5beda3b4c] <<< String de.scrum_master.app.Application.bar(String)
foobar
Thread 9[32c8444c-0f1f-4023-9b97-69d5beda3b4c] <<< void de.scrum_master.app.Application.1.run()
Thread 1[549d0856-0a92-4031-9331-a1317d6a43c4] <<< void de.scrum_master.app.Application.main(String[])
ご覧のとおり、スレッドには既に一意の ID があるため、UUID をまったく使用せずにアスペクトを実装することを検討してください。