Introduzione all'API StackWalking di Java 9

1. Introduzione

In questo rapido articolo, daremo un'occhiata all'API StackWalking di Java 9.

La nuova funzionalità permette di accedere ad un flusso di StackFrame s , che ci permette di pila facilmente navigare in direttamente e facendo buon uso del potente flusso API in Java 8.

2. Vantaggi di uno StackWalker

In Java 8, Throwable :: getStackTrace e Thread :: getStackTrace restituiscono un array di StackTraceElement . Senza molto codice manuale, non c'era modo di scartare i frame indesiderati e mantenere solo quelli che ci interessavano.

Oltre a ciò, Thread :: getStackTrace potrebbe restituire un'analisi parziale dello stack. Questo perché la specifica consente all'implementazione della VM di omettere alcuni stack frame per motivi di prestazioni.

In Java 9, utilizzando il metodo walk () dello StackWalker , possiamo attraversare alcuni frame che ci interessano o l'intera traccia dello stack.

Ovviamente, la nuova funzionalità è thread-safe; ciò consente a più thread di condividere una singola istanza StackWalker per accedere ai rispettivi stack.

Come descritto nel JEP-259, la JVM verrà migliorata per consentire un accesso pigro efficiente a stack frame aggiuntivi quando necessario.

3. StackWalker in azione

Cominciamo creando una classe contenente una catena di chiamate di metodo:

public class StackWalkerDemo { public void methodOne() { this.methodTwo(); } public void methodTwo() { this.methodThree(); } public void methodThree() { // stack walking code } }

3.1. Cattura l'intera traccia dello stack

Andiamo avanti e aggiungiamo un po 'di codice per camminare sullo stack:

public void methodThree() { List stackTrace = StackWalker.getInstance() .walk(this::walkExample); } 

Il metodo StackWalker :: walk accetta un riferimento funzionale, crea un flusso di StackFrame s per il thread corrente, applica la funzione al flusso e chiude il flusso .

Ora definiamo il metodo StackWalkerDemo :: walkExample :

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

Questo metodo raccoglie semplicemente lo StackFrame se lo restituisce come un elenco . Per provare questo esempio, esegui un test JUnit:

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

L'unico motivo per eseguirlo come test JUnit è avere più frame nel nostro stack:

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

Nell'intera traccia dello stack, siamo interessati solo ai primi quattro frame. I frame rimanenti da org.junit e org.eclipse non sono altro che frame di rumore .

3.2. Filtrare i StackFrame s

Miglioriamo il nostro codice di esplorazione dello stack e rimuoviamo il rumore:

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

Usando la potenza dell'API Stream , stiamo mantenendo solo i frame a cui siamo interessati. Ciò eliminerà il rumore, lasciando le prime quattro righe nel registro dello stack:

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

Identifichiamo ora il test JUnit che ha avviato la chiamata:

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

Tieni presente che qui siamo interessati solo a un singolo StackFrame, che è mappato su una stringa . L'output sarà solo la riga contenente la classe StackWalkerDemoTest .

3.3. Catturare i fotogrammi di riflessione

Per catturare i frame di riflessione, che sono nascosti per impostazione predefinita, StackWalker deve essere configurato con un'opzione aggiuntiva SHOW_REFLECT_FRAMES :

List stackTrace = StackWalker .getInstance(StackWalker.Option.SHOW_REFLECT_FRAMES) .walk(this::walkExample);

Usando questa opzione, verranno catturati tutti i frame di riflessione inclusi Method.invoke () e Constructor.newInstance () :

com.baeldung.java9.stackwalker.StackWalkerDemo#methodThree, Line 40 com.baeldung.java9.stackwalker.StackWalkerDemo#methodTwo, Line 16 com.baeldung.java9.stackwalker.StackWalkerDemo#methodOne, Line 12 com.baeldung.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

Come possiamo vedere, i frame jdk.internal sono quelli nuovi catturati dall'opzione SHOW_REFLECT_FRAMES .

3.4. Cattura di cornici nascoste

Oltre ai frame di riflessione, un'implementazione JVM può scegliere di nascondere frame specifici dell'implementazione.

Tuttavia, quei frame non sono nascosti dallo StackWalker :

Runnable r = () -> { List stackTrace2 = StackWalker .getInstance(StackWalker.Option.SHOW_HIDDEN_FRAMES) .walk(this::walkExample); printStackTrace(stackTrace2); }; r.run();

Si noti che in questo esempio stiamo assegnando un riferimento lambda a un Runnable . L'unico motivo è che JVM creerà alcuni frame nascosti per l'espressione lambda.

Questo è chiaramente visibile nella traccia dello stack:

com.baeldung.java9.stackwalker.StackWalkerDemo#lambda$0, Line 47 com.baeldung.java9.stackwalker.StackWalkerDemo$$Lambda$39/924477420#run, Line -1 com.baeldung.java9.stackwalker.StackWalkerDemo#methodThree, Line 50 com.baeldung.java9.stackwalker.StackWalkerDemo#methodTwo, Line 16 com.baeldung.java9.stackwalker.StackWalkerDemo#methodOne, Line 12 com.baeldung.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

I primi due frame sono i frame proxy lambda, che JVM ha creato internamente. Vale la pena notare che i frame di riflessione che abbiamo catturato nell'esempio precedente vengono ancora conservati con l' opzione SHOW_HIDDEN_FRAMES . Questo perché SHOW_HIDDEN_FRAMES è un superset di SHOW_REFLECT_FRAMES .

3.5. Identificazione della classe chiamante

L'opzione RETAIN_CLASS_REFERENCE vende al dettaglio l'oggetto della classe in tutti gli StackFrame percorsi dallo StackWalker . Questo ci permette di chiamare i metodi StackWalker :: getCallerClass e StackFrame :: getDeclaringClass .

Identifichiamo la classe chiamante utilizzando il metodo StackWalker :: getCallerClass :

public void findCaller() { Class caller = StackWalker .getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE) .getCallerClass(); System.out.println(caller.getCanonicalName()); }

Questa volta chiameremo questo metodo direttamente da un test JUnit separato:

@Test public void giveStalkWalker_whenInvokingFindCaller_thenFindCallingClass() { new StackWalkerDemo().findCaller(); }

L'output di caller.getCanonicalName (), sarà:

com.baeldung.java9.stackwalker.StackWalkerDemoTest

Notare che StackWalker :: getCallerClass non deve essere chiamato dal metodo in fondo allo stack. in quanto provocherà il lancio di IllegalCallerException .

4. Conclusione

Con questo articolo, abbiamo visto quanto sia facile gestire StackFrame utilizzando la potenza di StackWalker combinata con l' API Stream .

Naturalmente, ci sono varie altre funzionalità che possiamo esplorare, come saltare, eliminare e limitare gli StackFrame . La documentazione ufficiale contiene alcuni esempi concreti per casi d'uso aggiuntivi.

E, come sempre, puoi ottenere il codice sorgente completo per questo articolo su GitHub.