Comment Lire et Comprendre la Stacktrace en Java ?

May 23, 2019
Rédigé par

lire-comprendre-stacktrace-java

Lorsque quelque chose ne tourne pas rond dans une application Java en cours d'exécution, le premier indice est souvent l'impression de lignes à l'écran qui ressemblent à peu près à ce qui suit :

Exception in thread "main" java.lang.RuntimeException: Something has gone wrong, aborting!
    at com.myproject.module.MyProject.badMethod(MyProject.java:22)
    at com.myproject.module.MyProject.oneMoreMethod(MyProject.java:18)
    at com.myproject.module.MyProject.anotherMethod(MyProject.java:14)
    at com.myproject.module.MyProject.someMethod(MyProject.java:10)
    at com.myproject.module.MyProject.main(MyProject.java:6)

Ceci est une Stacktrace, et dans ce blog post, je vais vous expliquer de quoi il s'agit, comment ils sont générés et comment les lire et les comprendre. Si ça à déjà l’air pénible, lisez la suite...

Anatomie d'une Stacktrace

Habituellement, une Stacktrace s'affiche lorsqu'une Exception n'est pas gérée correctement dans le code. Il peut s'agir d'un des types d'exception intégrés de base dans Java, ou d'une exception customisée créée par un programme ou une librairie.

La Stacktrace contient le type d'exception et un message, ainsi qu'une liste de tous les appels de méthode qui étaient en cours au moment où l'exception a été levée.

C’est parti pour la dissection de la Stacktrace. La première ligne nous indique les détails de l'exception :

explication de la composition de la stacktrace

C'est un bon début. La ligne 2 montre quel code était en cours d’exécution lorsque le problème est arrivé :

explication d'un log en particulier

Ça nous aide à circonscrire le problème, mais quelle partie du code à  appelé badMethod? La réponse se trouve à la ligne suivante, qui peut être lue exactement de la même manière. Et comment en est-on arrivé là ? Regardez sur la ligne suivante. Et ainsi de suite, jusqu'à la dernière ligne, qui est la main method  (méthode principale) de l'application.  En lisant la Stacktrace de bas en haut, vous pouvez retracer le chemin exact depuis le début de votre code, jusqu'au moment de l'exception.

Qu'est-ce qui a mal tourné ?

meme d'une photo d'une maison qui brûle. Le texte dit Throw new House On Fire Exception ()

La cause d'une exception est généralement une instruction throw explicite. En utilisant le nom du fichier et le numéro de ligne, vous pouvez vérifier exactement quel ligne de code a généré l'exception. Cela ressemblera probablement à quelque chose comme ceci :

  throw new RuntimeException("Quelque chose s'est mal passé, on annule tout!");

C'est un bon endroit pour commencer à chercher le problème sous-jacent : y a-t-il des instructions “if” autour de cette ligne ? Que fait ce code ? D'où proviennent les données utilisées dans cette méthode ?

Il est également possible pour le code de lancer une exception sans qu’un throw explicite soit visible dans le code, par exemple vous pouvez obtenir :

  • NullPointerException si obj est null dans le code ce qui appelle obj.someMethod()
  • ArithmeticException arrive si on fait une division par zero avec des integers, ex: 1/0. Bizarrement, il n'y a pas d'exception si c'est avec des floats, 1.0/0.0 retourne infinity sans aucun problème!
  • NullPointerException si un nombre entier null est incrémenté en tant que int. ex: Integer a=null; a++;
  • Il existe d'autres exemples dans la documentation Java, c'est pourquoi il est important de savoir que les exceptions peuvent survenir sans être explicitement codées.

Traitement des exceptions générées par les librairies

L'une des grandes forces de Java est le grand nombre de librairies disponibles. Toute lib populaire sera bien testée donc lorsque vous êtes confrontés à une exception provenant d'une librairie, il est fort probable que l’erreur vienne de la façon dont nous l'utilisons et pas du code de la librairie lui-même.

Par exemple, si nous utilisons la classe Fraction d'Apache Commons Lang et que nous lui passons des paramètres comme suit :

  Fraction.getFraction(numberOfFoos, numberOfBars);

Si numberOfBars est égal à zéro, la Stacktrace sera comme ça :

Exception in thread "main" java.lang.ArithmeticException: The denominator must not be zero
    at org.apache.commons.lang3.math.Fraction.getFraction(Fraction.java:143)
    at com.project.module.MyProject.anotherMethod(MyProject.java:17)
    at com.project.module.MyProject.someMethod(MyProject.java:13)
    at com.project.module.MyProject.main(MyProject.java:9)

Beaucoup de bonnes librairies fournissent une Javadoc qui comprend des informations sur les types d'exceptions qui peuvent être thrown (levées) et pourquoi. Dans ce cas particulier Fraction.getFraction a documenté que ça lèvera une ArithmeticException si une fraction a un dénominateur zéro. Ici aussi, le message est clair, mais dans des situations plus complexes ou ambiguës, la doc peut être votre meilleure alliée.

Pour lire cette Stacktrace, commencez en haut avec le type d’Exception - ArithmeticException et le message message The denominator must not be zero(Le dénominateur ne doit pas être nul). Cela donne une idée de ce qui a mal tourné, mais pour découvrir le code qui a causé l'exception, parcourez la Stacktrace à la recherche de quelque chose dans le package com.myproject (c'est à la 3ème ligne ici), puis rendez-vous en fin de la ligne pour voir où se trouve le code (MyProject.java:17). Cette ligne contiendra du code qui s’appelle Fraction.getFraction. C'est le point de départ de l'enquête : qu'est-ce quels sont les paramètres passés à getFraction? D'où viennent-ils ?

Dans les gros projets comportant de nombreuses dépendances, les Stacktraces peuvent compter des centaines de lignes. Si vous voyez une grosse Stacktrace, entraînez-vous à parcourir la liste des at ... at ... at ... à la recherche de votre propre code - c'est une compétence utile à développer.

La bonne pratique : intercepter et relever les exceptions

Disons que nous travaillons sur un projet de taille conséquente qui traite des FooBars fictifs, et que notre code va être utilisé par d'autres. Nous pourrions décider d'interecpter l'ArithmeticException de Fraction et la lever nous même comme quelque chose de custom pour notre projet, ce qui donnerait :

   try {
            ....
           Fraction.getFraction(x,y);
            ....
   } catch ( ArithmeticException e ){
           throw new MyProjectFooBarException("Le nombre de FooBars ne peut être zéro", e);
   }

Utiliser cette technique présente plusieurs avantages :

  • Nos utilisateurs n'ont pas à se préoccuper de la ArithmeticException - ce qui nous donne la possibilité de modifier la façon dont commons-lang est utilisé.
  • Il est possible d'ajouter plus de contexte, en précisant par exemple quel est le nombre de FooBars à l'origine du problème.
  • Cela peut aussi rendre les Stacktraces plus faciles à lire, comme nous le verrons ci-dessous.

Il n'est pas nécessaire d’écrire son propre code pour gérer chaque exception, mais lorsqu'il semble y avoir une librairie entre deux couches de code à vous, comme l'appel à une lib, ça a souvent du sens.

Vous pouvez noter que le constructeur de MyProjectFooBarException prend 2 arguments : un message et l'exception qui l'a provoqué. Chaque exception en Java possède un champ cause, et lorsque vous interceptez et relevez l’Exception comme ici, vous devriez toujours le remplir pour aider les autres développeurs à déboguer les erreurs. La Stacktrace pourrait ressembler à ce qui suit :

Exception in thread "main" com.myproject.module.MyProjectFooBarException: The number of FooBars cannot be zero
    at com.myproject.module.MyProject.anotherMethod(MyProject.java:19)
    at com.myproject.module.MyProject.someMethod(MyProject.java:12)
    at com.myproject.module.MyProject.main(MyProject.java:8)
Caused by: java.lang.ArithmeticException: La dénominateur ne doit pas être zéro
    at org.apache.commons.lang3.math.Fraction.getFraction(Fraction.java:143)
    at com.myproject.module.MyProject.anotherMethod(MyProject.java:17)
    ... 2 more

L'exception la plus récente est sur la première ligne, et l'endroit où elle a été levée est toujours sur la ligne 2. Cependant, ce type de Stacktrace peut prêter à confusion car la méthode d’interception a changé l'ordre des appels de méthode par rapport aux Stacktraces que nous avons vus auparavant. La méthode principale n'est plus en bas de la pile, et le code qui a déclenché l'exception en premier lieu n'est plus en haut de la pile. Lorsque vous utilisez cette technique plusieurs fois dans votre code, c’est encore plus visible mais l’idée reste la même:

Vérifiez les sections de la première à la dernière en cherchant votre code, puis lisez les sections pertinentes de bas en haut.

Libraries vs frameworks

La différence entre une lib et un framework en Java est la suivante :

  • Votre code appelle des méthodes dans une librairie
  • Votre code est appelé par les méthodes d'un Framework

Un type courant de Framework est le serveur d'application web, comme SparkJava ou Spring Boot. En utilisant SparkJava et Commons-Lang, nous pourrions un jour voir une Stacktrace comme celle-ci :

com.framework.FrameworkException: Error in web request
    at com.framework.ApplicationStarter.lambda$start$0(ApplicationStarter.java:15)
    at spark.RouteImpl$1.handle(RouteImpl.java:72)
    at spark.http.matching.Routes.execute(Routes.java:61)
    at spark.http.matching.MatcherFilter.doFilter(MatcherFilter.java:134)
    at spark.embeddedserver.jetty.JettyHandler.doHandle(JettyHandler.java:50)
    at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:1568)
    at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:144)
    at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:132)
    at org.eclipse.jetty.server.Server.handle(Server.java:503)
    at org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:364)
    at org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:260)
    at org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:305)
    at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:103)
    at org.eclipse.jetty.io.ChannelEndPoint$2.run(ChannelEndPoint.java:118)
    at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:765)
    at org.eclipse.jetty.util.thread.QueuedThreadPool$2.run(QueuedThreadPool.java:683)
    at java.base/java.lang.Thread.run(Thread.java:834)
Caused by: com.project.module.MyProjectFooBarException: The number of FooBars cannot be zero
    at com.project.module.MyProject.anotherMethod(MyProject.java:20)
    at com.project.module.MyProject.someMethod(MyProject.java:12)
    at com.framework.ApplicationStarter.lambda$start$0(ApplicationStarter.java:13)
    ... 16 more
Caused by: java.lang.ArithmeticException: The denominator must not be zero
    at org.apache.commons.lang3.math.Fraction.getFraction(Fraction.java:143)
    at com.project.module.MyProject.anotherMethod(MyProject.java:18)
    ... 18 more

OK, c’est plutôt long. Comme avant, le premier suspect est notre propre code, mais il est de plus en plus difficile de trouver où il se trouve. En haut, il y a l'exception du Framework, en bas celle de la Bibliothèque et au milieu, notre propre code. Ouf !

Un Framework et complexe et une lib peuvent créer une douzaine ou plus de sections Caused by: (Causé par). Une bonne stratégie est de parcourir ces lignes à la recherche de votre propre code : Caused by: com.myproject... Lisez ensuite cette section en détail pour isoler le problème.

En résumé

Apprendre à comprendre les Stacktraces et à les lire rapidement vous permettra de localiser les problèmes et rendra le débogage beaucoup moins pénible. C'est une compétence qui s'améliore avec la pratique, alors la prochaine fois que vous verrez une grosse Stacktrace, ne soyez pas intimidé - il y a beaucoup d'informations extrêmement utiles si vous savez comment les lire.

Si vous avez des conseils ou des astuces sur la façon de traiter les Stacktraces Java, je serais ravi de les connaître. N'hésitez pas à me contacter par email : mgilliard@twilio.com.