Einführung in die Java 9 StackWalking-API

Einführung in die Java 9 StackWalking-API

1. Einführung

In diesem kurzen Artikel werden wir uns dieStackWalking APIvon Java 9 ansehen.

The new functionality provides access to a Stream of StackFrames, so dass wir den Stapel sowohl direkt durchsuchen als auch die leistungsstarkenStream API in Java 8 gut nutzen können.

2. Vorteile von aStackWalker

In Java 8 gebenThrowable::getStackTrace undThread::getStackTrace ein Array vonStackTraceElements zurück. Ohne viel manuellen Code gab es keine Möglichkeit, die unerwünschten Frames zu verwerfen und nur die zu behalten, die uns interessieren.

Zusätzlich können dieThread::getStackTrace eine teilweise Stapelverfolgung zurückgeben. Dies liegt daran, dass die VM-Implementierung in der Spezifikation einige Stack-Frames aus Gründen der Leistung weglassen kann.

In Java 9using the walk() method of the StackWalker, we can traverse a few frames that we are interested in oder die vollständige Stapelverfolgung.

Natürlich ist die neue Funktionalität threadsicher. Auf diese Weise können mehrere Threads eine einzelneStackWalker-Instanz gemeinsam nutzen, um auf ihre jeweiligen Stapel zuzugreifen.

Wie inJEP-259 beschrieben, wird die JVM erweitert, um bei Bedarf einen effizienten verzögerten Zugriff auf zusätzliche Stapelrahmen zu ermöglichen.

3. StackWalker in Aktion

Beginnen wir mit der Erstellung einer Klasse, die eine Kette von Methodenaufrufen enthält:

public class StackWalkerDemo {

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

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

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

3.1. Erfassen Sie die gesamte Stapelverfolgung

Lassen Sie uns fortfahren und einen Stack-Walking-Code hinzufügen:

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

Die MethodeStackWalker::walk akzeptiert eine Funktionsreferenz, erstelltStream vonStackFrames für den aktuellen Thread, wendet die Funktion aufStream an und schließtStream.

Definieren wir nun die MethodeStackWalkerDemo::walkExample:

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

Diese Methode sammelt einfach dieStackFrames und gibt sie alsList<StackFrame> zurück. Führen Sie zum Testen dieses Beispiels einen JUnit-Test durch:

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

Der einzige Grund, es als JUnit-Test auszuführen, besteht darin, mehr Frames in unserem Stack zu haben:

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

Im gesamten Stack-Trace interessieren uns nur die oberen vier Frames. Die restlichenframes from org.junit and org.eclipse are nothing but noise frames.

3.2. Filtern derStackFrames

Lassen Sie uns unseren Stack-Walking-Code verbessern und das Rauschen beseitigen:

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

Mit der Leistung derStream-API behalten wir nur die Frames bei, an denen wir interessiert sind. Dies löscht das Rauschen und lässt die oberen vier Zeilen im Stapelprotokoll:

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

Lassen Sie uns nun den JUnit-Test identifizieren, der den Aufruf ausgelöst hat:

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");
}

Bitte beachten Sie, dass wir hier nur an einem einzelnenStackFrame, interessiert sind, der einemString zugeordnet ist. Die Ausgabe ist nur die Zeile mit der KlasseStackWalkerDemoTest.

3.3. Erfassen der Reflexionsrahmen

Um die standardmäßig ausgeblendeten Reflexionsrahmen zu erfassen, müssenStackWalker mit einer zusätzlichen OptionSHOW_REFLECT_FRAMES konfiguriert werden:

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

Mit dieser Option werden alle Reflexionsrahmen einschließlichMethod.invoke() undConstructor.newInstance() erfasst:

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

Wie wir sehen können, sind diejdk.internal Frames die neuen, die von derSHOW_REFLECT_FRAMES Option erfasst werden.

3.4. Versteckte Frames erfassen

Zusätzlich zu den Reflection-Frames kann eine JVM-Implementierung festlegen, dass implementierungsspezifische Frames ausgeblendet werden.

Diese Frames sind jedoch nicht vor denStackWalker verborgen:

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

Beachten Sie, dass wir in diesem Beispiel einemRunnable eine Lambda-Referenz zuweisen. Der einzige Grund ist, dass JVM einige versteckte Frames für den Lambda-Ausdruck erstellt.

Dies ist im Stack-Trace deutlich sichtbar:

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

Die beiden oberen Frames sind die Lambda-Proxy-Frames, die JVM intern erstellt hat. Es ist zu beachten, dass die im vorherigen Beispiel erfassten Reflexionsrahmen weiterhin mit der OptionSHOW_HIDDEN_FRAMESbeibehalten werden. Dies liegt daran, dassSHOW_HIDDEN_FRAMES is a superset of SHOW_REFLECT_FRAMES.

3.5. Identifizieren der anrufenden Klasse

Die OptionRETAIN_CLASS_REFERENCE verkauft das Objekt vonClass in allenStackFrames, die vonStackWalker durchlaufen werden. Dies ermöglicht es uns, die MethodenStackWalker::getCallerClass undStackFrame::getDeclaringClass aufzurufen.

Identifizieren wir die aufrufende Klasse mit der MethodeStackWalker::getCallerClass:

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

Dieses Mal rufen wir diese Methode direkt aus einem separaten JUnit-Test auf:

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

Die Ausgabe voncaller.getCanonicalName(), ist:

com.example.java9.stackwalker.StackWalkerDemoTest

Bitte beachten Sie, dassStackWalker::getCallerClass nicht von der Methode am unteren Rand des Stapels aufgerufen werden sollte. da dies dazu führt, dassIllegalCallerException geworfen werden.

4. Fazit

In diesem Artikel haben wir gesehen, wie einfach es ist, mitStackFrames umzugehen, indem die Leistung vonStackWalker in Kombination mit derStream-API verwendet wird.

Natürlich gibt es verschiedene andere Funktionen, die wir untersuchen können - wie das Überspringen, Löschen und Begrenzen vonStackFrames. Dasofficial documentation enthält einige solide Beispiele für zusätzliche Anwendungsfälle.

Und wie immer können Sie den vollständigen Quellcode für diesen Artikelover on GitHub erhalten.