Introduction à l’API Java 9 StackWalking

Introduction à l'API Java 9 StackWalking

1. introduction

Dans cet article rapide, nous allons jeter un œil auxStackWalking API de Java 9.

The new functionality provides access to a Stream of StackFrames, nous permettant de parcourir facilement la pile à la fois directement et en utilisant à bon escient les puissantsStream API in Java 8.

2. Avantages d'unStackWalker

Dans Java 8, lesThrowable::getStackTrace etThread::getStackTrace renvoient un tableau deStackTraceElements. Sans beaucoup de code manuel, il n'y avait aucun moyen de supprimer les images non désirées et de ne conserver que celles qui nous intéressaient.

En plus de cela, lesThread::getStackTrace peuvent renvoyer une trace de pile partielle. En effet, la spécification permet à la mise en œuvre de la machine virtuelle d'omettre certaines trames de pile pour des raisons de performances.

Dans Java 9,using the walk() method of the StackWalker, we can traverse a few frames that we are interested in ou la trace de pile complète.

Bien sûr, la nouvelle fonctionnalité est thread-safe; cela permet à plusieurs threads de partager une seule instance deStackWalker pour accéder à leurs piles respectives.

Comme décrit dans lesJEP-259, la JVM sera améliorée pour permettre un accès paresseux efficace aux cadres de pile supplémentaires si nécessaire.

3. StackWalker en action

Commençons par créer une classe contenant une chaîne d'appels de méthode:

public class StackWalkerDemo {

    public void methodOne() {
        this.methodTwo();
    }

    public void methodTwo() {
        this.methodThree();
    }

    public void methodThree() {
        // stack walking code
    }
}

3.1. Capturer la trace de la pile entière

Allons de l'avant et ajoutons du code de marche de pile:

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

La méthodeStackWalker::walk accepte une référence fonctionnelle, crée unStream deStackFrames pour le thread courant, applique la fonction auxStream et ferme lesStream.

Définissons maintenant la méthodeStackWalkerDemo::walkExample:

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

Cette méthode collecte simplement lesStackFrames et les renvoie sous forme deList<StackFrame>. Pour tester cet exemple, exécutez un test JUnit:

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

La seule raison de l'exécuter en tant que test JUnit est d'avoir plus d'images dans notre pile:

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

Dans toute la trace de la pile, nous ne nous intéressons qu'aux quatre premiers cadres. Lesframes from org.junit and org.eclipse are nothing but noise frames restants.

3.2. Filtrer lesStackFrames

Améliorons notre code de marche de pile et supprimons le bruit:

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

En utilisant la puissance de l'APIStream, nous ne conservons que les frames qui nous intéressent. Cela éliminera le bruit en laissant les quatre premières lignes du journal de la pile:

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

Identifions maintenant le test JUnit qui a lancé l'appel:

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");
}

Veuillez noter qu'ici, nous ne sommes intéressés que par un seulStackFrame, qui est mappé sur unString. La sortie sera uniquement la ligne contenant la classeStackWalkerDemoTest.

3.3. Capture des cadres de réflexion

Afin de capturer les images de réflexion, qui sont masquées par défaut, lesStackWalker doivent être configurés avec une option supplémentaireSHOW_REFLECT_FRAMES:

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

En utilisant cette option, toutes les images de réflexions, y comprisMethod.invoke() etConstructor.newInstance() seront capturées:

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

Comme nous pouvons le voir, les imagesjdk.internal sont les nouvelles capturées par l'optionSHOW_REFLECT_FRAMES.

3.4. Capture de cadres cachés

En plus des images de réflexion, une implémentation JVM peut choisir de masquer des images spécifiques à l’implémentation.

Cependant, ces cadres ne sont pas cachés desStackWalker:

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

Notez que nous attribuons une référence lambda à unRunnable dans cet exemple. La seule raison est que JVM créera des images cachées pour l'expression lambda.

Ceci est clairement visible dans la trace de la pile:

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

Les deux cadres supérieurs sont les cadres proxy lambda, créés par la machine virtuelle Java. Il est intéressant de noter que les cadres de réflexion que nous avons capturés dans l'exemple précédent sont toujours conservés avec l'optionSHOW_HIDDEN_FRAMES. C'est parce queSHOW_HIDDEN_FRAMES is a superset of SHOW_REFLECT_FRAMES.

3.5. Identifier la classe appelante

L'optionRETAIN_CLASS_REFERENCE détaille l'objet deClass dans tous lesStackFrame parcourus par lesStackWalker. Cela nous permet d'appeler les méthodesStackWalker::getCallerClass etStackFrame::getDeclaringClass.

Identifions la classe appelante en utilisant la méthodeStackWalker::getCallerClass:

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

Cette fois, nous allons appeler cette méthode directement à partir d'un test JUnit distinct:

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

La sortie decaller.getCanonicalName(), sera:

com.example.java9.stackwalker.StackWalkerDemoTest

Veuillez noter que lesStackWalker::getCallerClass ne doivent pas être appelés à partir de la méthode en bas de la pile. car cela entraînera le rejet deIllegalCallerException.

4. Conclusion

Avec cet article, nous avons vu à quel point il est facile de gérer lesStackFrames en utilisant la puissance desStackWalker combinée à l'APIStream.

Bien sûr, il existe diverses autres fonctionnalités que nous pouvons explorer - telles que sauter, supprimer et limiter lesStackFrames. Leofficial documentation contient quelques exemples solides pour des cas d'utilisation supplémentaires.

Et, comme toujours, vous pouvez obtenir le code source complet de cet articleover on GitHub.