StackWalker – oder wer hat mich aufgerufen?

Aus aktuellem Grund möchten wir uns in dieser Folge der Java 9 Perlen das StackWalker API anschauen. In einer Debugging Session in einem aktuellen Projekt wollten wir gerne wissen, von wem eine Methode aufgerufen wird. Und dabei sind wir auf den StackWalker getroffen.

Wir gehen von folgender simplen Klasse mit einer Main Methode aus:

public class StackTraceJdk8 {

  public void a() {
    b();
  }

  public void b() {
    c();
  }

  public void c() {
    // (*)
  }       

  public static void main(final String[] args) {
    new StackTraceJdk8().a();
  }
}

Innerhalb der Methode c() möchten wir wissen, wer diese aufgerufen hat (d.h. wir möchten den “execution stack”).

Java 8 und früher

Schon seit Java 5 gibt es die Möglichkeit, einen StackTrace via Thread.currentThread().getStackTrace() zu generieren. Wir erhalten dann ein Array von StackTraceElement[], welches wir mit Java 8 Stream elegant weiter verarbeiten können. Die StackTraceElement Elemente bieten Zugriff auf ClassName, MethodName, LineNumer und eine nette Implementation der toString() Methode.

Dies kann man (*) folgendermassen implementieren:

Arrays.stream(Thread.currentThread().getStackTrace()) 
      .forEach(System.out::println);

Damit erhalten wir eine Ausgabe in folgender Form:

java.base/java.lang.Thread.getStackTrace(Thread.java:1654)
ch.puzzle.stackwalker.StackTraceJdk8.printStackframes1(StackTraceJdk8.java:42)
ch.puzzle.stackwalker.StackTraceJdk8.c(StackTraceJdk8.java:32)
ch.puzzle.stackwalker.StackTraceJdk8.b(StackTraceJdk8.java:27)
ch.puzzle.stackwalker.StackTraceJdk8.a(StackTraceJdk8.java:23)
ch.puzzle.stackwalker.StackTraceJdk8.main(StackTraceJdk8.java:60)

Es ist leicht zu erkennen, dass die Aufruf Hierarchie main -> a -> b -> c ist.
Mit dem Java 8 Stream API kann man die Ausgabe weiter anpassen. Hier z,B, interessieren die beiden ersten Zeilen nicht wirklich, vielleicht möchte man auch die Anzahl Zeilen begrenzen (auf maxSize) etc. …

Arrays.stream(Thread.currentThread().getStackTrace()) 
    .skip(2) 
    .limit(maxSize) 
    .forEach(System.out::println);

Man kann auch einfach StackTraceElement nach Klassennamen filtern:

String filter = “ch.puzzle”;
Arrays.stream(Thread.currentThread().getStackTrace()) 
    .filter(f -> f.getClassName().contains(filter)) 
    .forEach(System.out::println);

Warum man nach Klassennamen filtern möchte leuchtet schnell ein, wenn man a() nicht via Main Methode sondern via Junit  einen Test ausführt.

public class StackWalkerTest {

	@Test
	public void test_java8() {
		new StackTraceJdk8().a();
	}
}

Ohne Filterung erhalten wir folgende Ausgabe (stark gekürzt von original 59 Zeilen):

java.base/java.lang.Thread.getStackTrace(Thread.java:1654)
ch.puzzle.stackwalker.StackTraceJdk8.printStackframes1(StackTraceJdk8.java:42)
ch.puzzle.stackwalker.StackTraceJdk8.c(StackTraceJdk8.java:32)
ch.puzzle.stackwalker.StackTraceJdk8.b(StackTraceJdk8.java:27)
ch.puzzle.stackwalker.StackTraceJdk8.a(StackTraceJdk8.java:23)
ch.puzzle.stackwalker.StackWalkerTest.test_java8(StackWalkerTest.java:14)
java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke)…
java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke…
java.base/java.lang.reflect.Method.invoke(Method.java:564)
org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:389)
org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:115)
org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:170)
org.eclipse.jdt.internal.junit5.runner.JUnit5TestReference.run(JUnit5TestReference.java:86)
org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)

Da uns die Interna von Junit und Eclipse nicht interessieren, können wir alle Klassen die nicht “ch.puzzle” enthalten einfach ausfiltern:

ch.puzzle.stackwalker.StackTraceJdk8.printStackframes3(StackTraceJdk8.java:54)
ch.puzzle.stackwalker.StackTraceJdk8.c(StackTraceJdk8.java:38)
ch.puzzle.stackwalker.StackTraceJdk8.b(StackTraceJdk8.java:27)
ch.puzzle.stackwalker.StackTraceJdk8.a(StackTraceJdk8.java:23)
ch.puzzle.stackwalker.StackWalkerTest.test_java8(StackWalkerTest.java:14)

Java 9

Mit Java 9 kommt das neue API „StackWalker“ zum Verarbeiten von StackTraces hinzu. Aber wozu ein neues API?

Das neue API bietet mehrere Vorteile gegenüber der Java 8 Lösung:

Mit Thread.currentThread().getStackTrace() bekommen wir den ganzen StackTrace als Array zurück. Wie im Junit Beispiel gesehen, kann dieser sehr lange werden. Mit dem neuen API kann man speicher-effizient nur durch die Teile des StackTraces wandern, die wirklich interessieren (“lazily evaluation of frames”).
Es ist nun möglich auf die Klasse zuzugreifen, in der die Methode deklariert ist, die wir im StackTrace sehen (“obtaining the caller class”).

Der einfachste Einstieg ins API ist es mit getInstance() einen StackWalker Object zu erzeugen und mit forEach über die einzelnen Elemente zu iterieren. Zu beachten ist, dass wir neu mit StackFrames und nicht mehr StackTraceElements arbeiten.

StackWalker.getInstance().forEach(System.out::println);

Neben der sehr einfachen forEach Variante gibt es noch eine walk() Methode, mit der man “gefiltert” über den StackTrace wandern kann.

API

public T walk(Function<? super Stream, ? extends T> function)
Applies the given function to the stream of StackFrames for the current thread, traversing from the top frame of the stack, which is the method calling this walk method.

Es gibt nun verschiedene Möglichkeiten walk() zu implementieren. Hier ein paar Vorschläge:

• Mit einer Methoden Referenz auf eine walkFunction() Methode, die das Function Interface implementiert. walkFunction() konvertiert dabei einfach die Werte des Streams in eine Liste:

StackWalker.getInstance() 
    .walk(this::walkFunction) 
    .forEach(System.out::println);

private List walkFunction(Stream stackFrameStream) {
  return stackFrameStream.collect(Collectors.toList());
}

Mit einer walkFunction() Methode, die selbst eine “Funkion” zurückgibt, die das Function Interface implementiert:

StackWalker.getInstance() 
      .walk(walkFunction()) 
      .forEach(System.out::println);

private Function<Stream, List> walkFunction() {
  return (stream) -> stream.collect(Collectors.toList());
}

Und natürlich können wir innerhalb des Streams auch wieder filter(), limit() etc. verwenden.

Standardmässig werden gewisse StackFrames ausgeblendet. Diese Ausblendung kann man aber anpassen, indem man beim Erstellen des StackWalkers einen “Options “ Parameter mitgibt. Verschiedene Werte werden unterstützt:

• Anzeige “Reflection Frames”. Dies kann man sehr schön nachvollziehen, indem man obigen Junit Test mit bzw. ohne SHOW_REFLECT_FRAMES Option aufruft.

StackWalker 
   .getInstance(StackWalker.Option.SHOW_REFLECT_FRAMES).walk(this::walkFunction) 
   .forEach(System.out::println);

Anzeige “Hidden Frames”. Damit werden Frames von “Lambdas” auch angezeigt. Erwähnenswert ist noch, dass SHOW_HIDDEN_FRAMES ein Superset von SHOW_REFLECT_FRAMES ist und damit beide anzeigt.

StackWalker 
   .getInstance(StackWalker.Option.SHOW_REFLECT_FRAMES).walk(this::walkFunction) 
   .forEach(System.out::println);

• Und schliesslich gibt es noch die Möglichkeit die Klasse zu bestimmen, die eine bestimmte Methode aufgerufen hat. Dafür muss man die Option RETAIN_CLASS_REFERENCE.

Class<?> caller = StackWalker //
    .getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE) //
    .getCallerClass();

Soviel zum Thema StackWalker. Mal schauen wohin uns die nächste Folge der Java 9 Perlen führen wird.

Referenzen:

• Michael Inden: Java 9 – Die Neuerungen, dpunkt.de
• baeldung.com
javaworld
Source Code

Kommentare sind geschlossen.