1. 序章

この簡単な記事では、Java9のStackWalkingAPIについて説明します。

新しい機能により、StackFrames のストリームにアクセスできるため、直接スタックを簡単に参照でき、Java8の強力なStreamAPIを活用できます。

2. StackWalkerの利点

Java 8では、 Throwable ::getStackTraceおよびThread:: getStackTrace は、StackTraceElementの配列を返します。 多くの手動コードがなければ、不要なフレームを破棄して、関心のあるフレームだけを保持する方法はありませんでした。

これに加えて、 Thread ::getStackTraceは部分的なスタックトレースを返す場合があります。 これは、仕様により、VM実装がパフォーマンスのために一部のスタックフレームを省略できるためです。

Java 9 では、StackWalkerのwalk()メソッドを使用して、または完全なスタックトレースに関心のあるいくつかのフレームをトラバースできます。

もちろん、新しい機能はスレッドセーフです。 これにより、複数のスレッドが単一の StackWalker インスタンスを共有して、それぞれのスタックにアクセスできるようになります。

JEP-259 で説明されているように、JVMは拡張され、必要に応じて追加のスタックフレームに効率的にレイジーアクセスできるようになります。

3. StackWalkerの動作

メソッド呼び出しのチェーンを含むクラスを作成することから始めましょう:

public class StackWalkerDemo {

    public void methodOne() {
        this.methodTwo();
    }

    public void methodTwo() {
        this.methodThree();
    }

    public void methodThree() {
        // stack walking code
    }
}

3.1. スタックトレース全体をキャプチャする

先に進んで、スタックウォーキングコードを追加しましょう。

public void methodThree() {
    List<StackFrame> stackTrace = StackWalker.getInstance()
      .walk(this::walkExample);
}

StackWalker :: walk メソッドは関数参照を受け入れ、現在のスレッドのStackFrameStreamを作成し、関数をStream[に適用しますX182X]、Streamを閉じます。

次に、 StackWalkerDemo ::walkExampleメソッドを定義しましょう。

public List<StackFrame> walkExample(Stream<StackFrame> stackFrameStream) {
    return stackFrameStream.collect(Collectors.toList());
}

このメソッドは単に収集します StackFrame sそしてそれをとして返しますリスト 。 この例をテストするには、JUnitテストを実行してください。

@Test
public void giveStalkWalker_whenWalkingTheStack_thenShowStackFrames() {
    new StackWalkerDemo().methodOne();
}

JUnitテストとして実行する唯一の理由は、スタックにフレームを追加することです。

class com.baeldung.java9.stackwalker.StackWalkerDemo#methodThree, Line 20
class com.baeldung.java9.stackwalker.StackWalkerDemo#methodTwo, Line 15
class com.baeldung.java9.stackwalker.StackWalkerDemo#methodOne, Line 11
class com.baeldung.java9.stackwalker
  .StackWalkerDemoTest#giveStalkWalker_whenWalkingTheStack_thenShowStackFrames, Line 9
class org.junit.runners.model.FrameworkMethod$1#runReflectiveCall, Line 50
class org.junit.internal.runners.model.ReflectiveCallable#run, Line 12
  ...more org.junit frames...
class org.junit.runners.ParentRunner#run, Line 363
class org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference#run, Line 86
  ...more org.eclipse frames...
class org.eclipse.jdt.internal.junit.runner.RemoteTestRunner#main, Line 192

スタックトレース全体では、上位4つのフレームのみに関心があります。 org.junitとorg.eclipseの残りのフレームは、ノイズフレームに他なりません。

3.2. StackFrameのフィルタリング

スタックウォーキングコードを強化して、ノイズを取り除きましょう。

public List<StackFrame> walkExample2(Stream<StackFrame> stackFrameStream) {
    return stackFrameStream
      .filter(f -> f.getClassName().contains("com.baeldung"))
      .collect(Collectors.toList());
}

Stream APIの機能を使用して、関心のあるフレームのみを保持しています。 これにより、ノイズが除去され、スタックログの上位4行が残ります。

class com.baeldung.java9.stackwalker.StackWalkerDemo#methodThree, Line 27
class com.baeldung.java9.stackwalker.StackWalkerDemo#methodTwo, Line 15
class com.baeldung.java9.stackwalker.StackWalkerDemo#methodOne, Line 11
class com.baeldung.java9.stackwalker
  .StackWalkerDemoTest#giveStalkWalker_whenWalkingTheStack_thenShowStackFrames, Line 9

次に、呼び出しを開始したJUnitテストを特定しましょう。

public String walkExample3(Stream<StackFrame> stackFrameStream) {
    return stackFrameStream
      .filter(frame -> frame.getClassName()
        .contains("com.baeldung") && frame.getClassName().endsWith("Test"))
      .findFirst()
      .map(f -> f.getClassName() + "#" + f.getMethodName() 
        + ", Line " + f.getLineNumber())
      .orElse("Unknown caller");
}

ここでは、 Stringにマップされる単一のStackFrame、のみに関心があることに注意してください。 出力は、StackWalkerDemoTestクラスを含む行のみになります。

3.3. リフレクションフレームのキャプチャ

デフォルトで非表示になっている反射フレームをキャプチャするには、StackWalkerを追加オプションSHOW_REFLECT_FRAMESで構成する必要があります。

List<StackFrame> stackTrace = StackWalker
  .getInstance(StackWalker.Option.SHOW_REFLECT_FRAMES)
  .walk(this::walkExample);

このオプションを使用すると、 Method.invoke()および Constructor.newInstance()を含むすべての反射フレームがキャプチャされます。

com.baeldung.java9.stackwalker.StackWalkerDemo#methodThree, Line 40
com.baeldung.java9.stackwalker.StackWalkerDemo#methodTwo, Line 16
com.baeldung.java9.stackwalker.StackWalkerDemo#methodOne, Line 12
com.baeldung.java9.stackwalker
  .StackWalkerDemoTest#giveStalkWalker_whenWalkingTheStack_thenShowStackFrames, Line 9
jdk.internal.reflect.NativeMethodAccessorImpl#invoke0, Line -2
jdk.internal.reflect.NativeMethodAccessorImpl#invoke, Line 62
jdk.internal.reflect.DelegatingMethodAccessorImpl#invoke, Line 43
java.lang.reflect.Method#invoke, Line 547
org.junit.runners.model.FrameworkMethod$1#runReflectiveCall, Line 50
  ...eclipse and junit frames...
org.eclipse.jdt.internal.junit.runner.RemoteTestRunner#main, Line 192

ご覧のとおり、 jdk.internal フレームは、SHOW_REFLECT_FRAMESオプションによってキャプチャされた新しいフレームです。

3.4. 隠しフレームのキャプチャ

リフレクションフレームに加えて、JVM実装は実装固有のフレームを非表示にすることを選択できます。

ただし、これらのフレームはStackWalkerから隠されていません。

Runnable r = () -> {
    List<StackFrame> stackTrace2 = StackWalker
      .getInstance(StackWalker.Option.SHOW_HIDDEN_FRAMES)
      .walk(this::walkExample);
    printStackTrace(stackTrace2);
};
r.run();

この例では、ラムダ参照をRunnableに割り当てていることに注意してください。 唯一の理由は、JVMがラムダ式の非表示フレームを作成することです。

これは、スタックトレースにはっきりと表示されます。

com.baeldung.java9.stackwalker.StackWalkerDemo#lambda$0, Line 47
com.baeldung.java9.stackwalker.StackWalkerDemo$$Lambda$39/924477420#run, Line -1
com.baeldung.java9.stackwalker.StackWalkerDemo#methodThree, Line 50
com.baeldung.java9.stackwalker.StackWalkerDemo#methodTwo, Line 16
com.baeldung.java9.stackwalker.StackWalkerDemo#methodOne, Line 12
com.baeldung.java9.stackwalker
  .StackWalkerDemoTest#giveStalkWalker_whenWalkingTheStack_thenShowStackFrames, Line 9
jdk.internal.reflect.NativeMethodAccessorImpl#invoke0, Line -2
jdk.internal.reflect.NativeMethodAccessorImpl#invoke, Line 62
jdk.internal.reflect.DelegatingMethodAccessorImpl#invoke, Line 43
java.lang.reflect.Method#invoke, Line 547
org.junit.runners.model.FrameworkMethod$1#runReflectiveCall, Line 50
  ...junit and eclipse frames...
org.eclipse.jdt.internal.junit.runner.RemoteTestRunner#main, Line 192

上位2つのフレームは、JVMが内部で作成したラムダプロキシフレームです。 前の例でキャプチャした反射フレームは、SHOW_HIDDEN_FRAMESオプションを使用しても保持されることに注意してください。 これは、SHOW_HIDDEN_FRAMESがSHOW_REFLECT_FRAMESのスーパーセットであるためです。

3.5. 呼び出し側クラスの識別

オプションRETAIN_CLASS_REFERENCEは、StackWalkerが歩くすべてのStackFrameClassのオブジェクトを小売りします。 これにより、メソッド StackWalker ::getCallerClassおよびStackFrame::getDeclaringClassを呼び出すことができます。

StackWalker ::getCallerClassメソッドを使用して呼び出し元のクラスを識別しましょう。

public void findCaller() {
    Class<?> caller = StackWalker
      .getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
      .getCallerClass();
    System.out.println(caller.getCanonicalName());
}

今回は、このメソッドを別のJUnitテストから直接呼び出します。

@Test
public void giveStalkWalker_whenInvokingFindCaller_thenFindCallingClass() {
    new StackWalkerDemo().findCaller();
}

caller.getCanonicalName()、の出力は次のようになります。

com.baeldung.java9.stackwalker.StackWalkerDemoTest

StackWalker :: getCallerClass は、スタックの最下部にあるメソッドから呼び出されるべきではないことに注意してください。 IllegalCallerExceptionがスローされるためです。

4. 結論

この記事では、StackWalkerStream APIを組み合わせて使用することで、StackFrameを簡単に処理できることを確認しました。

もちろん、 StackFrame のスキップ、ドロップ、制限など、他にもさまざまな機能を検討できます。 公式ドキュメントには、追加のユースケースの確かな例がいくつか含まれています。

そして、いつものように、GitHubでこの記事の完全なソースコードを入手できます。