IntelliJ : Démystification du débogueur

La nouvelle cuvée IntelliJ est disponible ! Elle apporte des nouveautés, mais ce ne sera pas le sujet de cet article. On passe beaucoup de temps dans son éditeur à écrire du code, mais il n’est pas rare d’en passer encore plus à le déboguer. Souvent, ce sont les fonctions de base qui sont utilisées (placer un point d’arrêt, avancer à la ligne suivante, etc), mais le débogueur d’IntelliJ est plus que ça. Nous allons parcourir ensemble ses fonctionnalités de base, ainsi que les plus avancées (breakpoint conditionnel, contrôle de l’exécution, etc).

Versions

Les exemples suivants sont basés sur la version IntelliJ Ultimate 13. Ils doivent tous être fonctionnels avec la dernière version Community d’IntelliJ, à part le dernier exemple, qui utilise Javascript. Tout devrait également être disponible avec la version précédente : IntelliJ 12.

Back To Basics

La JVM embarque plusieurs méthodes pour démarrer un programme en mode debug, et l’exécuter pas à pas.

La plus simple : demander à l’IDE de lancer votre code, et il se chargera de la configuration de la JVM (ouverture d’un port sur la JVM, connexion à ce port, etc). Pour cela, faites un clic droit sur votre méthode main, une méthode de test, un job maven, ou tout autre code qu’IntelliJ peut exécuter, puis sélectionnez “Debug” dans le menu.

Petite astuce : si vous souhaitez qu’il ne soit possible de démarrer qu’une unique instance de votre programme, faites un edit de la configuration nouvellement créée, puis cochez “Single Instance Only”. Ainsi, si vous relancez cette configuration en debug, sans avoir arrêté votre précédente session, IntelliJ l’arrêtera avant de démarrer la nouvelle. Fini le problème classique du “Port Already In Use”.

Si vous ne voulez pas, ou ne pouvez pas lancer votre application depuis IntelliJ, vous pouvez lui demander de se connecter à un port distant, préalablement ouvert, via les options de la JVM :

-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=1234

Cette méthode d’accroche est utile lorsque vous ne pouvez pas lancer votre applicatif depuis IntelliJ (serveur d’application non supporté), ou lorsqu’il ne tourne pas sur votre machine (serveur d’intégration distant).

Pour se connecter à un port d’ouvert, il faut ajouter une configuration de type Remote (via le menu Run > Edit Configuration > + > Remote). Mettez à jour l’host et le port, et vous voilà prêts pour déboguer votre application distante.

Si vous utilisez un serveur d’applications/web supporté par IntelliJ, il est également capable de le lancer directement en mode debug. Pour cela, il suffit de rajouter une configuration, de la même façon que pour Remote. L’avantage de cette méthode est de n’avoir aucune configuration à modifier sur votre serveur pour ouvrir un port, puisque IntelliJ s’en charge.

Maintenant que notre application est lancée en mode debug, il faut y placer des points d’arrêt, qui nous permettront de mettre en pause le programme et d’inspecter l’état des différents objets, variables, etc.

Points d’arrêt

Line breakpoint

Le point d’arrêt sur une ligne est le point d’arrêt classique : un clic dans la gouttière de l’éditeur de code suffit à en ajouter un. C’est le seul point d’arrêt qu’il n’est pas possible d’ajouter via un menu, même si l’aide d’IntelliJ indique le contraire.

Exception Breakpoint

Quand une exception est levée, on peut mettre un point sur la ligne lançant l’exception, mais il est également possible de demander au préalable à l’IDE de s’arrêter quand une exception est lancée. Pour ajouter un point d’arrêt de ce type (ou des types suivants de cet article), passez par le menu Run > View Breakpoints > + > Exception Breakpoint puis choisissez le type de votre exception.

Lorsque l’exception est levée, l’IDE s’arrête et se positionne sur la ligne concernée. On remarquera le point rouge avec l’éclair, pour représenter un point d’arrêt de type Harry Potter Exception.

Pour traquer une exception, il est souvent plus pratique et plus facile de passer par ce type de point d’arrêt que d’en mettre sur toutes les lignes.

Method Breakpoint

Il est possible de mettre un point d’arrêt quand on entre, ou sort, d’une méthode spécifique, en utilisant le type de point d’arrêt Method Breakpoint. IntelliJ offre la possibilité d’en placer sur une méthode spécifique (en cliquant dans la gouttière sur la ligne de déclaration d’une méthode), ou sur un ensemble de méthodes, en passant par le menu “View Breakpoints” (Ctrl + Maj + F8).

Lors de l’ajout du point d’arrêt de type Method, IntelliJ demande un nom de classe (cf 1). On peut être restrictif en spécifiant une classe unique, comme par exemple hello.AppServer, ou plus large, hello.*. Attention tout de même : hello.App* ne marche pas. Il faut ensuite indiquer le nom de la méthode (cf 2), mais il n’est pas possible d’utiliser de joker dans ce dernier champ.

Attention, ce type de point d’arrêt ralentit considérablement l’exécution du programme. Il faut donc l’utiliser avec parcimonie.

Field Watchpoint

Le dernier type de point d’arrêt est le Field Watchpoint, qui permet d’être notifié lors de la lecture et/ou l’écriture d’un champ d’un objet. Pour en ajouter un, vous pouvez passer par le menu “View Breakpoints” (Ctrl + Maj + F8) ou faire un clic droit sur le champ concerné (1) et sélectionner Toggle Field Watchpoint (2)

Lors de l’exécution, IntelliJ s’arrêtera si un accès, ou une écriture, s’effectue sur ce champ, ce qui est pratique pour tracer qui/quoi modifie votre objet.

Configuration des points d’arrêt

Nous avons vu la liste des différents types de breakpoint dans IntelliJ. Leur usage est classique, mais la configuration de ces points d’arrêt peut grandement aider dans le débogage.

Log lors du passage sur un point d’arrêt

Sur différentes missions, j’ai souvent vu des logs de type “Je suis passé par ici”, “je suis passé par là” utilisés pour déboguer le déroulement d’un programme. Via la configuration d’un point d’arrêt de type ligne, il est possible d’avoir exactement le même comportement sans avoir à modifier le code : pas de perte de temps de recompilation, plus de risque d’oublier du code inutile, etc.

Pour avoir le même comportement, il faut désactiver l’interruption d’un thread (1) lors du passage sur le point d’arrêt : c’est facultatif, mais après tout, vous ne voulez pas forcement vous arrêter à chaque fois. Il est ensuite possible de configurer le log que l’on veut (2), pour obtenir une trace indiquant qu’on est passé par ce point d’arrêt, ou configurer la trace via une expression (ici : “Contenu du message =>” + this.message), ce qui donne les résultats ci-dessous :

Stop selon une condition

IntelliJ permet d’arrêter le programme sur un point d’arrêt selon un critère pré-défini. Le cas d’usage courant est le parcours d’un nombre important d’objets dans une liste : comment faire pour ne s’arrêter que sur l’objet courant ayant un champ avec une valeur spécifique ?

Sur la capture d’écran, la condition indique que le programme doit s’arrêter si le champ “message” commence par la chaîne “Robert”. (1). Lorsque vous écrivez une condition, IntelliJ connaît le contexte autour de votre condition, donc n’hésitez pas à profiter de l’auto-complétion avec Ctrl+Espace.

Filtres

Je passe rapidement sur les filtres, car j’en trouve la mise en œuvre trop complexe pour être facilement utilisable dans un environnement quotidien (et je ne lui ai pas encore trouvé de réelle utilité).

Il est possible de conditionner un point d’arrêt sur une instance précise (1), en indiquant la référence de celle-ci dans la configuration du point d’arrêt. Cependant, cela oblige à connaître la référence avant la configuration, ce qui est pénible en pratique et me pousse donc à laisser cette fonctionnalité de côté. L’utilisation d’une condition sur un critère spécifique est, à mes yeux, plus simple à mettre en oeuvre.

Pour filtrer sur un type de classe (classe spécifique, pattern, etc), il faut utiliser Class filters (2), même si je n’ai pas encore trouvé d’utilité directe à ce filtre. Un usage envisageable sur un point d’arrêt de type Exception, serait de ne s’arrêter que si l’exception est lancée dans un package appartenant à votre projet, et ainsi ne pas être pollué par des exceptions lancées par des frameworks tiers.

Le dernier filtre, Pass Count (3), permet de définir le nombre de passages effectués avant l’activation du point d’arrêt. Usage typique : dans une boucle avec un nombre d’éléments connu, indiquer au bout de combien d’itérations le programme doit s’arrêter.

Condition d’activation inter-breakpoints.

Il arrive d’avoir un code à déboguer qui soit commun à beaucoup d’appels dans l’application, et c’est uniquement en passant dans un certain embranchement que nous voudrions que IntelliJ s’arrête. Pour résoudre ce problème, on peut mettre le point d’arrêt au niveau du code qui nous intéresse, le désactiver, et mettre un point d’arrêt dans l’embranchement concerné. Une fois ce dernier atteint, on réactive le précédent point d’arrêt. Cependant, tout cela demande une certaine mécanique intellectuelle, entre le risque de s’y perdre et l’énergie dépensée à activer/désactiver nos points d’arrêt.

Le même comportement peut être obtenu via IntelliJ, mais bien plus facilement. Placez un point d’arrêt dans votre embranchement. Activez ou désactivez la suspension du programme sur ce point d’arrêt à votre bon vouloir. Placez ensuite un second point d’arrêt dans votre code commun, et configurez-le pour qu’il ne s’active QUE si on passe par votre premier point d’arrêt (1).

Plus besoin d’activer/désactiver les points d’arrêt dans tous les sens : IntelliJ le fait pour vous. Petite remarque : par défaut, IntelliJ désactive votre point d’arrêt conditionnel une fois qu’il est atteint. En situation réelle, j’ai souvent tendance à le laisser activé. N’oubliez donc pas de modifier l’option.(2)

Contrôle de l’exécution

Il existe différentes actions de contrôle de l’exécution de votre programme.

  • Step Over (1) : Permet de passer à la ligne suivante
  • Step Into (2) : Permet de rentrer dans le prochain appel de méthode
  • (Force) Step Into (3) : Permet également de rentrer dans le prochain appel de méthode. La nuance est que IntelliJ exclut un certain nombre de classes pour le Step Into (exemple : String) pour éviter de descendre trop bas dans le JDK. Force Step Into enlève cette limite.
  • Step Out (4) : Permet de sortir directement de la méthode
  • Run To Cursor (5) : exécute le code jusqu’à atteindre le curseur.

Smart Step Into (Maj + F7) est une action qui n’est pas affichée sur l’interface, pourtant je la trouve bien plus pratique que Force Step Into (que je n’utilise jamais…) : dans un appel de méthode imbriqué, la sélection de la méthode dans laquelle nous voulons faire un Step Into est proposée via cette fonctionnalité. Une fois que l’on sait cela, il n’est alors pas obligatoire de faire un Step Into dans toutes les méthodes pour atteindre celle qui nous intéresse. Cette fonction n’est pas assez mise en avant dans l’IDE, et c’est bien dommage.

Il peut arriver qu’on clique un peu trop vite sur Step Over : comment revenir en arrière ? On pourrait tout recommencer, mais c’est très fastidieux. Pour revenir en arrière dans l’exécution, il faut revenir au cran précédent, au niveau de la pile d’appel (Drop Frame (6)), ce qui nous fait revenir à la dernière exécution de méthode. C’est presque comme remonter dans le temps, à la nuance près que toutes les modifications globales ont eu lieu : si une variable non locale a été modifiée, elle restera modifiée après l’utilisation de Drop Frame.

Inspection des variables

Une fois votre code en erreur atteint, il faut regarder l’état des différentes variables. IntelliJ propose une liste d’outils intéressants.

Vous pouvez inspecter une variable (1), voir son contenu, etc, et si elle est intéressante pour vous, et que vous voulez voir son évolution dans le temps, vous pouvez l’ajouter dans la liste des watches (2) en la sélectionnant, puis clic droit et Add To Watches.

Il est également possible de faire de l’évaluation de code (3), ce qui est pratique pour tester en direct des expressions comme String.format(“%d”, myVariable) ou String.format(“%f”, maVariable). Un usage un peu moins conventionnel : quand une exception est interceptée, faire myException.printStackTrace() dans l’évaluateur de code permet de récupérer rapidement la stack dans la console.

Si l’on veut tester le comportement de notre code en injectant des valeurs différentes, on peut modifier celle d’une variable, ou d’un objet, en effectuant un clic droit dessus, et en sélectionnant Set Value. Il suffit alors d’assigner la valeur souhaitée, même pour l’assignation d’un nouvel objet : new DomaineObject(“blabla”).

Une fonctionnalité moins connue, mais très pratique du débogueur est la possibilité d’accéder à la déclaration d’une variable : faites un clic droit sur la variable puis sélectionnez Jump to Source pour la déclaration, ou Jump to Object Source pour la définition.

Par défaut, IntelliJ utilise, pour l’afficher dans le panneau des variables, la méthode toString d’un objet, qui n’est pas toujours surchargée, ce qui engendre l’affichage de références. Cet affichage est modifiable par type de classe, via un mécanisme de “Data Views”, accessible grâce à un clic droit sur un objet > Customize Data View. Dans le nouveau panneau qui apparaît, sélectionnez Data Type Renders, et ajoutez un nouveau “renderers” : il s’agit d’une nouvelle définition à utiliser pour l’affichage dans le débogueur d’un objet.

On sélectionne tout d’abord la classe dont on veut changer l’affichage dans le débogueur (1), puis on indique l’expression à utiliser pour obtenir une chaîne de caractères à afficher. (2) Ici, je demande à IntelliJ d’afficher les dates avec au format français (3) à la place du format ISO (4).

Cette dernière fonctionnalité est très pratique, notamment avec des objets métier n’ayant pas une représentation utilisable lors d’un debug : l’ajout d’un Data Renderer simplifie l’exploitation du débogueur rendant les objets plus lisibles.

Déboguer…autre chose ?

Le débogueur marche très bien avec tous les langages qui tournent sur la JVM, tels que Groovy et Scala, pour ceux que j’ai essayés. Il est également capable de déboguer du Javascript, si vous préférez le débogueur de votre IDE à celui de votre navigateur web. Pour cela, il faut créer une configuration de type JavaScript (1) qui n’est disponible qu’avec la version Ultimate d’IntelliJ. Dans le cas où IntelliJ n’arriverait pas à faire le mapping entre vos sources et le JavaScript exécuté par le navigateur, indiquez l’url distante de vos fichiers en local (2). Au lancement de cette configuration, IntelliJ démarrera votre navigateur et vous demandera, la première fois, d’installer un plugin pour que celui-ci communique avec votre IDE.

Toutes les fonctionnalités, comme le Data Type Renderer, ne sont pas disponibles mais, si tout se passe normalement, vous devriez pouvoir, depuis IntelliJ, manipuler les variables, contrôler l’exécution, etc.

Happy Debugging 🙂

Nombre de vue : 514

COMMENTAIRES 2 commentaires

  1. François dit :

    Salut, article intéressant, mais je ne suis pas d’accord concernant l’ajout de breakpoint par menu. IntelliJ le permet par raccourcis (ctrl F8) ce qui fait partie des innombrables “features” qui font l’attrait d’intelliJ.

  2. David Wursteisen dit :

    Bonjour,

    Ce que je voulais dire, c’est surtout qu’en passant via le menu Breakpoints (Ctrl + Maj + F8), il n’est pas possible de rajouter un point d’arrêt type ligne.

    Mon cas d’usage (de mémoire) : ajouter un point d’arrêt dans du code dont je n’ai pas le source : Vu que je n’ai pas les sources, je n’ai pas les lignes correspondantes et donc, je ne peux pas ajouter le point d’arrêt.

    Pourtant j’aimerais y placer un point d’arrêt pour inspecter les variables.

    Dans ce cas, je suis dans l’impossibilité d’ajouter mon point d’arrêt. Je peux tout de même passer par un point d’arrêt

    Je trouve juste dommage que l’on puisse ajouter tous les types de point d’arrêt via un menu, sauf le point d’arrêt type ligne : avec une popup qui demande une classe et un numéro de ligne.

    Ca ne doit pas être un cas d’usage courant car personne ne le demande sur le youtrack d’IntelliJ

    Cdt

AJOUTER UN COMMENTAIRE