Java 9 StackWalking APIの紹介

Java 9 StackWalking APIの概要

1. 前書き

この簡単な記事では、Java 9のStackWalking APIを見ていきます。

The new functionality provides access to a Stream of StackFramesを使用すると、スタックを直接参照することも、強力なStream API in Java 8をうまく利用することもできます。

2. StackWalkerの利点

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

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

Java 9では、using the walk() method of the StackWalker, we can traverse a few frames that we are interested inまたは完全なスタックトレース。

もちろん、新しい機能はスレッドセーフです。これにより、複数のスレッドがそれぞれのスタックにアクセスするための単一の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 stackTrace = StackWalker.getInstance()
      .walk(this::walkExample);
}

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

それでは、StackWalkerDemo::walkExampleメソッドを定義しましょう。

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

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

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

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

class com.example.java9.stackwalker.StackWalkerDemo#methodThree, Line 20
class com.example.java9.stackwalker.StackWalkerDemo#methodTwo, Line 15
class com.example.java9.stackwalker.StackWalkerDemo#methodOne, Line 11
class com.example.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つのフレームのみに関心があります。 残りのframes from org.junit and org.eclipse are nothing but noise frames

3.2. StackFramesのフィルタリング

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

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

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

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

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

public String walkExample3(Stream stackFrameStream) {
    return stackFrameStream
      .filter(frame -> frame.getClassName()
        .contains("com.example") && 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 stackTrace = StackWalker
  .getInstance(StackWalker.Option.SHOW_REFLECT_FRAMES)
  .walk(this::walkExample);

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

com.example.java9.stackwalker.StackWalkerDemo#methodThree, Line 40
com.example.java9.stackwalker.StackWalkerDemo#methodTwo, Line 16
com.example.java9.stackwalker.StackWalkerDemo#methodOne, Line 12
com.example.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 stackTrace2 = StackWalker
      .getInstance(StackWalker.Option.SHOW_HIDDEN_FRAMES)
      .walk(this::walkExample);
    printStackTrace(stackTrace2);
};
r.run();

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

これは、スタックトレースで明確に確認できます。

com.example.java9.stackwalker.StackWalkerDemo#lambda$0, Line 47
com.example.java9.stackwalker.StackWalkerDemo$$Lambda$39/924477420#run, Line -1
com.example.java9.stackwalker.StackWalkerDemo#methodThree, Line 50
com.example.java9.stackwalker.StackWalkerDemo#methodTwo, Line 16
com.example.java9.stackwalker.StackWalkerDemo#methodOne, Line 12
com.example.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 is a superset of SHOW_REFLECT_FRAMESが原因です。

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

オプションRETAIN_CLASS_REFERENCEは、StackWalkerが歩いたすべてのStackFrames内のClassのオブジェクトを小売りします。 これにより、メソッド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.example.java9.stackwalker.StackWalkerDemoTest

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

4. 結論

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

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

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