Введение в API Java 9 StackWalking

Введение в Java 9 StackWalking API

1. Вступление

В этой быстрой статье мы рассмотримStackWalking API в Java 9.

The new functionality provides access to a Stream of StackFrames, что позволяет нам легко просматривать стек как напрямую, так и эффективно использовать мощныйStream API in Java 8.

2. Преимущества aStackWalker

В Java 8Throwable::getStackTrace иThread::getStackTrace возвращают массивStackTraceElements. Без большого количества ручного кода не было бы возможности отбросить ненужные кадры и оставить только те, которые нам интересны.

В дополнение к этомуThread::getStackTrace может возвращать частичную трассировку стека. Это связано с тем, что спецификация позволяет реализации виртуальной машины пропускать некоторые кадры стека ради производительности.

В Java 9using 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 принимает функциональную ссылку, создаетStream изStackFrames для текущего потока, применяет функцию к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

Во всей трассировке стека нас интересуют только четыре верхних кадра. Остальные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());
}

Используя возможности APIStream, мы сохраняем только те кадры, которые нам интересны. Это уберет шум, оставив четыре верхние строки в журнале стека:

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

Обратите внимание, что здесь нас интересует только одинStackFrame,, который отображается вString. На выходе будет только строка, содержащая класс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

Два верхних фрейма - это лямбда-прокси-фреймы, которые JVM создала для себя. Стоит отметить, что кадры отражения, которые мы захватили в предыдущем примере, все еще сохраняются с опциейSHOW_HIDDEN_FRAMES. Это потому, чтоSHOW_HIDDEN_FRAMES is a superset of SHOW_REFLECT_FRAMES.

3.5. Определение вызывающего класса

ОпцияRETAIN_CLASS_REFERENCE продает объектClass в розницу во всехStackFrames, пройденныхStackWalker. Это позволяет нам вызывать методы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. Заключение

В этой статье мы увидели, насколько легко справиться сStackFrames, используя возможностиStackWalker в сочетании с APIStream.

Конечно, мы можем изучить и другие различные функции, такие как пропуск, удаление и ограничениеStackFrames. official documentation содержит несколько убедительных примеров для дополнительных вариантов использования.

И, как всегда, вы можете получить полный исходный код этой статьиover on GitHub.