Introdução à API Java 9 StackWalking
1. Introdução
Neste artigo rápido, daremos uma olhada emStackWalking API do Java 9.
The new functionality provides access to a Stream of StackFrames, permitindo-nos navegar facilmente na pilha diretamente e fazendo bom uso do poderosoStream API in Java 8.
2. Vantagens de aStackWalker
No Java 8,Throwable::getStackTraceeThread::getStackTrace retorna uma matriz deStackTraceElements. Sem muito código manual, não havia como descartar os quadros indesejados e manter apenas aqueles nos quais estamos interessados.
Além disso, oThread::getStackTrace pode retornar um rastreamento de pilha parcial. Isso ocorre porque a especificação permite que a implementação da VM omita alguns quadros de pilha por uma questão de desempenho.
No Java 9,using the walk() method of the StackWalker, we can traverse a few frames that we are interested in ou o rastreamento de pilha completo.
Obviamente, a nova funcionalidade é segura para thread; isso permite que vários threads compartilhem uma única instânciaStackWalker para acessar suas respectivas pilhas.
Conforme descrito emJEP-259, a JVM será aprimorada para permitir acesso lento eficiente a quadros de pilha adicionais, quando necessário.
3. StackWalker em ação
Vamos começar criando uma classe contendo uma cadeia de chamadas de método:
public class StackWalkerDemo {
public void methodOne() {
this.methodTwo();
}
public void methodTwo() {
this.methodThree();
}
public void methodThree() {
// stack walking code
}
}
3.1. Capture o rastreamento de pilha inteira
Vamos seguir em frente e adicionar alguns códigos de stack walking:
public void methodThree() {
List stackTrace = StackWalker.getInstance()
.walk(this::walkExample);
}
O métodoStackWalker::walk aceita uma referência funcional, cria umStream deStackFrames para o encadeamento atual, aplica a função aoStreame fecha oStream.
Agora vamos definir o métodoStackWalkerDemo::walkExample:
public List walkExample(Stream stackFrameStream) {
return stackFrameStream.collect(Collectors.toList());
}
Este método simplesmente coletaStackFrames e o retorna comoList<StackFrame>. Para testar este exemplo, execute um teste JUnit:
@Test
public void giveStalkWalker_whenWalkingTheStack_thenShowStackFrames() {
new StackWalkerDemo().methodOne();
}
O único motivo para executá-lo como um teste JUnit é ter mais quadros em nossa pilha:
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
Em todo o rastreamento da pilha, estamos interessados apenas nos quatro principais quadros. Osframes from org.junit and org.eclipse are nothing but noise frames restantes.
3.2. Filtrando oStackFrames
Vamos aprimorar nosso stack walking code e remover o ruído:
public List walkExample2(Stream stackFrameStream) {
return stackFrameStream
.filter(f -> f.getClassName().contains("com.example"))
.collect(Collectors.toList());
}
Usando o poder da APIStream, estamos mantendo apenas os frames nos quais estamos interessados. Isso eliminará o ruído, deixando as quatro principais linhas no log da pilha:
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
Agora vamos identificar o teste JUnit que iniciou a chamada:
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");
}
Observe que aqui, estamos interessados apenas em um únicoStackFrame, mapeado paraString. A saída será apenas a linha que contém a classeStackWalkerDemoTest.
3.3. Capturando as molduras de reflexão
Para capturar os quadros de reflexão, que estão ocultos por padrão, oStackWalker precisa ser configurado com uma opção adicionalSHOW_REFLECT_FRAMES:
List stackTrace = StackWalker
.getInstance(StackWalker.Option.SHOW_REFLECT_FRAMES)
.walk(this::walkExample);
Usando esta opção, todos os quadros de reflexos incluindoMethod.invoke()eConstructor.newInstance() serão capturados:
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
Como podemos ver, os quadrosjdk.internal são os novos capturados pela opçãoSHOW_REFLECT_FRAMES.
3.4. Capturando quadros ocultos
Além dos quadros de reflexão, uma implementação da JVM pode optar por ocultar quadros específicos da implementação.
No entanto, esses quadros não estão ocultos doStackWalker:
Runnable r = () -> {
List stackTrace2 = StackWalker
.getInstance(StackWalker.Option.SHOW_HIDDEN_FRAMES)
.walk(this::walkExample);
printStackTrace(stackTrace2);
};
r.run();
Observe que estamos atribuindo uma referência lambda aRunnable neste exemplo. O único motivo é que a JVM criará alguns quadros ocultos para a expressão lambda.
Isso é claramente visível no rastreamento da pilha:
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
Os dois quadros principais são os quadros de proxy lambda, que a JVM criou internamente. Vale a pena notar que os quadros de reflexão que capturamos no exemplo anterior ainda são retidos com a opçãoSHOW_HIDDEN_FRAMES. Isso ocorre porqueSHOW_HIDDEN_FRAMES is a superset of SHOW_REFLECT_FRAMES.
3.5. Identificando a classe chamadora
A opçãoRETAIN_CLASS_REFERENCE vende o objeto deClass em todos osStackFrames percorridos porStackWalker. Isso nos permite chamar os métodosStackWalker::getCallerClasseStackFrame::getDeclaringClass.
Vamos identificar a classe de chamada usando o métodoStackWalker::getCallerClass:
public void findCaller() {
Class> caller = StackWalker
.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
.getCallerClass();
System.out.println(caller.getCanonicalName());
}
Desta vez, chamaremos esse método diretamente de um teste JUnit separado:
@Test
public void giveStalkWalker_whenInvokingFindCaller_thenFindCallingClass() {
new StackWalkerDemo().findCaller();
}
A saída decaller.getCanonicalName(), será:
com.example.java9.stackwalker.StackWalkerDemoTest
Observe queStackWalker::getCallerClass não deve ser chamado a partir do método na parte inferior da pilha. pois isso resultará emIllegalCallerException sendo lançado.
4. Conclusão
Com este artigo, vimos como é fácil lidar comStackFrames usando o poder deStackWalker combinado com a APIStream.
Claro, existem várias outras funcionalidades que podemos explorar - como pular, descartar e limitar oStackFrames. Oofficial documentation contém alguns exemplos sólidos para casos de uso adicionais.
E, como sempre, você pode obter o código-fonte completo deste artigoover on GitHub.