Introdução à API Java 9 StackWalking

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.