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 .