Перейти к основному содержимому

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

· 5 мин. чтения

1. Введение

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

Новая функциональность обеспечивает доступ к потоку StackFrame , что позволяет нам легко просматривать стек как напрямую, так и с пользой использовать мощный Stream API в Java 8 .

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

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

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

В Java 9, используя метод walk() StackWalker , мы можем просмотреть несколько интересующих нас кадров или полную трассировку стека.

Конечно, новая функциональность ориентирована на многопотоковое исполнение; это позволяет нескольким потокам совместно использовать один экземпляр 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 принимает функциональную ссылку, создает поток StackFrame для текущего потока, применяет функцию к потоку и закрывает поток .

Теперь определим метод StackWalkerDemo::walkExample :

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

Этот метод просто собирает StackFrame и возвращает его как List<StackFrame> . Чтобы протестировать этот пример, запустите тест JUnit:

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

Единственная причина запустить его как тест JUnit — иметь больше кадров в нашем стеке:

class com.foreach.java9.stackwalker.StackWalkerDemo#methodThree, Line 20
class com.foreach.java9.stackwalker.StackWalkerDemo#methodTwo, Line 15
class com.foreach.java9.stackwalker.StackWalkerDemo#methodOne, Line 11
class com.foreach.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

Во всей трассировке стека нас интересуют только четыре верхних кадра. Остальные кадры из org.junit и org.eclipse — не что иное, как шумовые кадры .

3.2. Фильтрация StackFrame _

Давайте улучшим наш код обхода стека и удалим шум:

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

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

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

Давайте теперь определим тест JUnit, который инициировал вызов:

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

Обратите внимание, что здесь нас интересует только один 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.foreach.java9.stackwalker.StackWalkerDemo#methodThree, Line 40
com.foreach.java9.stackwalker.StackWalkerDemo#methodTwo, Line 16
com.foreach.java9.stackwalker.StackWalkerDemo#methodOne, Line 12
com.foreach.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.foreach.java9.stackwalker.StackWalkerDemo#lambda$0, Line 47
com.foreach.java9.stackwalker.StackWalkerDemo$$Lambda$39/924477420#run, Line -1
com.foreach.java9.stackwalker.StackWalkerDemo#methodThree, Line 50
com.foreach.java9.stackwalker.StackWalkerDemo#methodTwo, Line 16
com.foreach.java9.stackwalker.StackWalkerDemo#methodOne, Line 12
com.foreach.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 является надмножеством SHOW_REFLECT_FRAMES .

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

Опция RETAIN_CLASS_REFERENCE продает объект Class во всех StackFrame , которые посещает 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.foreach.java9.stackwalker.StackWalkerDemoTest

Обратите внимание, что StackWalker::getCallerClass не следует вызывать из метода в нижней части стека. так как это приведет к созданию исключения IllegalCallerException .

4. Вывод

В этой статье мы увидели, как легко работать со StackFrame, используя мощь StackWalker в сочетании с Stream API.

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

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