Debogage Efficace des Exceptions

Autres langues : English Español Deutsch 日本語 한국어 Português 中文

Dans votre parcours avec Java, l’un des premiers concepts a apprendre est celui des exceptions. Ce terme definit un scenario inattendu pendant l’execution d’un programme, comme une panne reseau ou une fin de fichier inattendue. Ou, comme le dit la documentation Oracle :

La classe Exception et ses sous-classes sont une forme de Throwable qui indique des conditions qu’une application raisonnable pourrait vouloir intercepter.

Si votre programme est equipe d’un “plan B” pour gerer efficacement ces situations, il continuera a fonctionner sans probleme, que l’une d’entre elles se produise ou non. Sinon, le programme peut planter de maniere inattendue ou se retrouver dans un etat incorrect.

Lorsqu’un programme echoue a cause d’une exception, vous devrez le deboguer. Les langages de programmation facilitent le debogage des erreurs liees aux exceptions en fournissant une trace de pile - un message special pointant vers le chemin de code qui a conduit a l’echec. Cette information est incroyablement utile, et parfois suffisante ; cependant, il y a des cas ou nous pourrions beneficier de details et techniques supplementaires.

Dans cet article, nous allons parcourir une etude de cas, en nous concentrant sur le debogage d’une exception qui se produit pendant l’analyse JSON. Ce faisant, nous irons au-dela de la simple lecture des traces de pile et decouvrirons les avantages de l’utilisation du debogueur.

Application exemple

L’application exemple pour ce cas d’utilisation sera un petit programme Java qui analyse un ensemble de fichiers JSON contenant des donnees sur les aeroports du monde entier. Les fichiers incluent des details, tels que le code IATA des aeroports, le pays, la latitude et la longitude. Voici un exemple d’entree :

{
    "iso_country": "AR",
    "iata_code": "MDQ",
    "longitude_deg": "-57.5733",
    "latitude_deg": "-37.9342",
    "elevation_ft": "72",
    "name": "Ástor Piazzola International Airport",
    "municipality": "Mar del Plata",
    "iso_region": "AR-B"
}

Le programme est assez simple. Il itere sur un ensemble de fichiers, lit et analyse chacun d’eux, filtre les objets aeroport selon des restrictions d’entree, telles que "country=AR", puis affiche la liste des aeroports correspondants :

public class Airports {

    static Path input = Paths.get("./data");
    static Gson gson = new Gson();
    static {
        System.setOut(new PrintStream(System.out, true, StandardCharsets.UTF_8));
    }

    public static void main(String[] args) throws IOException {

        List<Predicate<Airport>> filters = new ArrayList<>();

        for (String arg : args) {
            if (arg.startsWith("country=")) {
                filters.add(airport -> airport.isoCountry.equals(arg.substring("country=".length()))) ;
            } else if (arg.startsWith("region=")) {
                filters.add(airport -> airport.isoRegion.equals(arg.substring("region=".length()))) ;
            } else if (arg.startsWith("municipality=")) {
                filters.add(airport -> airport.municipality.equals(arg.substring("municipality=".length()))) ;
            }
        }

        try (Stream<Path> files = Files.list(input)) {
            Stream<Airport> airports = files.map(Airports::parse);
            for (Predicate<Airport> f : filters) {
                airports = airports.filter(f);
            }
            airports.forEach(System.out::println);
        }
    }

    static Airport parse(Path path) {
        try {
            JsonObject root = gson.fromJson(Files.readString(path), JsonObject.class);
            String name =           root.get("name").getAsString();
            String isoCountry =     root.get("isoCountry").getAsString();
            String iataCode =       root.get("iataCode").getAsString();
            String longitudeDeg =   root.get("longitudeDeg").getAsString();
            String latitudeDeg =    root.get("latitudeDeg").getAsString();
            String municipality =   root.get("municipality").getAsString();
            String isoRegion =      root.get("isoRegion").getAsString();
            Integer elevationFt =   Integer.parseInt(root.get("elevationFt").getAsString());
            return new Airport(name, isoCountry, iataCode, longitudeDeg, latitudeDeg, elevationFt, municipality, isoRegion);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    record Airport(String name, String isoCountry, String iataCode, String longitudeDeg, String latitudeDeg, Integer elevationFt, String municipality, String isoRegion) { }
}

Le probleme

Lorsque nous executons le programme, il echoue avec une NumberFormatException . Voici la trace de pile que nous obtenons :

Exception in thread "main" java.lang.NumberFormatException: For input string: ""
	at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
	at java.base/java.lang.Integer.parseInt(Integer.java:672)
	at java.base/java.lang.Integer.parseInt(Integer.java:778)
	at dev.flounder.Airports.parse(Airports.java:53)
	at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197)
	at java.base/java.util.Iterator.forEachRemaining(Iterator.java:133)
	at java.base/java.util.Spliterators$IteratorSpliterator.forEachRemaining(Spliterators.java:1939)
	at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509)
	at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
	at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:151)
	at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:174)
	at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
	at java.base/java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:596)
	at dev.flounder.Airports.main(Airports.java:39)

Elle pointe vers la ligne 53 du fichier Airports.java . En regardant cette ligne, nous pouvons dire qu’il y a un probleme de conversion de la propriete elevationFt en nombre pour l’un des aeroports. Le message d’exception nous dit que c’est parce que cette propriete est vide dans le fichier correspondant.

La trace de pile n’est-elle pas suffisante ?

Bien que la trace de pile ci-dessus nous donne deja beaucoup d’informations, elle est limitee de plusieurs facons. Elle pointe vers les lignes de code qui echouent, mais nous dit-elle quel fichier de donnees est la cause ? Le nom du fichier serait utile si nous voulons l’inspecter de plus pres ou corriger les donnees. Malheureusement, les details d’execution comme ceux-ci sont perdus lorsque l’application plante.

Il y a cependant un moyen de suspendre l’application avant qu’elle ne se termine, ce qui vous permet d’acceder au contexte qui n’est pas capture dans les traces de pile et les logs. De plus, avec une application suspendue, vous pouvez prototyper, tester, et meme charger la correction pendant que l’application est encore en cours d’execution.

Suspendre a une exception

Pour commencer, definissons un point d’arret d’exception. Vous pouvez le faire soit dans la boite de dialogue des points d’arret soit en cliquant sur Create breakpoint directement dans la trace de pile :

'Créer un point d'arrêt' est affiché près de la trace de pile dans la console 'Créer un point d'arrêt' est affiché près de la trace de pile dans la console

Plutot que de cibler une ligne specifique, ce type de point d’arret suspend l’application juste avant qu’une exception soit levee. Actuellement, nous nous interessons a NumberFormatException .

Une ligne surlignée indique que le point d'arrêt a fonctionné et que le programme a été suspendu. Une ligne surlignée indique que le point d'arrêt a fonctionné et que le programme a été suspendu.

Donc, nous avons suspendu l’application lorsqu’elle allait lever l’exception. L’erreur s’est deja produite, mais l’application n’a pas encore plante. Nous arrivons juste a temps, voyons donc ce que cela nous donne.

Dans l’onglet Threads, allez au cadre parse() :

Sélectionner le cadre 'parse()' dans l'onglet 'Threads' du débogueur. Sélectionner le cadre 'parse()' dans l'onglet 'Threads' du débogueur.

Regardez combien plus d’informations nous avons maintenant : nous voyons le nom du fichier ou l’erreur se produit, le contenu complet du fichier, toutes les variables environnantes, et, bien sur, la propriete qui cause l’erreur. Il devient evident que le probleme est dans le fichier appele 816.json parce qu’il manque la propriete elevationFt

Nous pouvons maintenant proceder a la correction du probleme. Selon notre cas d’utilisation, nous pourrions vouloir simplement corriger les donnees, ou corriger la facon dont le programme gere l’erreur. Corriger les donnees est simple, voyons donc comment le debogueur peut nous aider avec la gestion des erreurs.

Prototyper la correction

Evaluer l’expression est un excellent outil pour prototyper des modifications, y compris des corrections a votre code existant. Exactement ce que nous cherchons. Selectionnez l’ensemble du corps de la methode parse() et allez a Run | Debugging Actions | Evaluate Expression.

Tip icon

Si vous selectionnez du code avant d’ouvrir la boite de dialogue Evaluate, l’extrait selectionne est copie, vous n’avez donc pas a le saisir manuellement.

Lorsque nous evaluons le code de la methode, il leve une exception, tout comme dans le code du programme :

L'évaluation du même code défaillant dans la boîte de dialogue 'Évaluer' entraîne également une NumberFormatException. L'évaluation du même code défaillant dans la boîte de dialogue 'Évaluer' entraîne également une NumberFormatException.

Changeons un peu le code en lui faisant stocker un null lorsqu’il rencontre des donnees manquantes. Ajoutons aussi une instruction d’affichage pour sortir l’erreur sur le flux d’erreur standard :

try {
    JsonObject root = gson.fromJson(Files.readString(path), JsonObject.class);
    String name =           root.get("name").getAsString();
    String isoCountry =     root.get("isoCountry").getAsString();
    String iataCode =       root.get("iataCode").getAsString();
    String longitudeDeg =   root.get("longitudeDeg").getAsString();
    String latitudeDeg =    root.get("latitudeDeg").getAsString();
    String municipality =   root.get("municipality").getAsString();
    String isoRegion =      root.get("isoRegion").getAsString();
    Integer elevationFt;
    try {
        elevationFt = Integer.parseInt(root.get("elevationFt").getAsString());
    } catch (NumberFormatException e) {
        elevationFt = null;
        System.err.println("Failed to parse elevation for file: " + path);
    }
    return new Airport(name, isoCountry, iataCode, longitudeDeg, latitudeDeg, elevationFt, municipality, isoRegion);
} catch (IOException e) {
    throw new RuntimeException(e);
}

En executant le code ajuste dans la boite de dialogue Evaluate, nous pouvons verifier que ce code gere vraiment l’erreur comme prevu. Maintenant il definit le champ correspondant a null au lieu de faire planter l’application :

Après avoir cliqué sur 'Évaluer' pour le code corrigé, le résultat montre une valeur de retour valide. Après avoir cliqué sur 'Évaluer' pour le code corrigé, le résultat montre une valeur de retour valide.

Non seulement nous pouvons tester que la methode retourne sans lever l’exception, mais aussi voir a quoi ressemblera le texte d’erreur dans la console :

La console dit 'Échec de l'analyse de l'élévation pour le fichier : ./data/816.json'. La console dit 'Échec de l'analyse de l'élévation pour le fichier : ./data/816.json'.

Reinitialiser le cadre

Ok, nous avons corrige le code pour que l’erreur ne se produise plus a l’avenir, mais pouvons-nous annuler l’erreur elle-meme ? En fait, oui ! Le debogueur d’IntelliJ IDEA nous permet de retirer le cadre defectueux de la pile et d’executer la methode depuis le debut.

Cliquez sur Reset Frame dans l’onglet Threads :

En pointant vers l'icône 'Réinitialiser le cadre' dans le cadre 'parse()' de l'onglet 'Threads' En pointant vers l'icône 'Réinitialiser le cadre' dans le cadre 'parse()' de l'onglet 'Threads'
Info icon

Reset Frame ne fait que restaurer l’etat interne d’un cadre. Dans notre cas, la methode est pure, ce qui signifie qu’elle ne change rien en dehors de sa portee locale, donc ce n’est pas un probleme. Sinon, si des changements sont apportes a l’etat global de l’application, ceux-ci ne seront pas annules. Gardez ces effets a l’esprit lorsque vous utilisez des fonctionnalites comme Evaluate et Reset Frame

Corriger et recharger a chaud

Apres avoir rejete l’erreur et fait comme si elle ne s’etait jamais produite, nous pouvons aussi livrer la correction a l’execution. Puisque le point d’execution est actuellement en dehors de la methode que nous voulons changer, et que nous n’avons pas change la signature de la methode, nous pouvons utiliser l’option Reload Changed Classes, que j’ai deja couverte dans l’un des articles precedents.

D’abord, copiez le code corrige de la boite de dialogue Evaluate vers l’editeur. Vous pouvez trouver le code precedemment evalue en parcourant l’historique (Option+Fleche bas / Alt+Fleche bas). Apres avoir remplace le code dans la classe Airports , vous pouvez le charger dans la JVM en cours d’execution. Pour cela, selectionnez Run | Debugging Actions | Reload Changed Classes.

Une bulle apparaît disant que les classes ont été rechargées. Une bulle apparaît disant que les classes ont été rechargées.

Une bulle apparait confirmant que la correction a ete chargee dans l’application en cours d’execution.

Tip icon

Si vous utilisez la version EAP actuelle d’IntelliJ IDEA (2024.3 EAP1), essayez le nouveau bouton de rechargement a chaud, qui apparait directement dans l’editeur :

Une fenêtre contextuelle apparaît dans le coin supérieur droit de l'éditeur demandant de recharger les fichiers. Une fenêtre contextuelle apparaît dans le coin supérieur droit de l'éditeur demandant de recharger les fichiers.

Filtre d’exception non interceptee

Si nous reprenons l’application maintenant, elle sera a nouveau suspendue a la meme exception. Pourquoi cela se produit-il ?

Nous avons modifie le code pour intercepter NumberFormatException . Cette correction empeche les plantages de l’application, mais elle n’empeche pas l’exception d’etre levee. Donc, le point d’arret se declenche toujours chaque fois que l’exception est levee, meme si elle sera finalement interceptee.

Disons a IntelliJ IDEA que nous voulons suspendre l’application uniquement lorsqu’une exception non interceptee se produit. Pour cela, faites un clic droit sur le point d’arret dans la marge et decochez la case Caught exception :

Case 'Caught exception' décochée dans la boîte de dialogue 'Point d'arrêt d'exception' qui s'est ouverte en cliquant sur l'icône de point d'arrêt dans la gouttière Case 'Caught exception' décochée dans la boîte de dialogue 'Point d'arrêt d'exception' qui s'est ouverte en cliquant sur l'icône de point d'arrêt dans la gouttière
Tip icon

L’icone du point d’arret d’exception n’apparait dans la marge que lorsque l’application est suspendue a une exception. Alternativement, vous pouvez configurer les points d’arret via Run | View Breakpoints.

Avec cette configuration, l’application s’executera sans interruption a moins qu’une NumberFormatException non geree ne se produise.

Reprendre et profiter

Nous pouvons maintenant reprendre l’application :

Pointer sur le bouton 'Reprendre le programme' dans la barre d'outils du débogueur Pointer sur le bouton 'Reprendre le programme' dans la barre d'outils du débogueur

Elle s’execute parfaitement, nous notifiant des donnees manquantes dans la console. Notez que 816.json est dans la liste des erreurs, confirmant que nous avons bien traite ce fichier, pas simplement saute.

La console affiche la sortie de l'application ainsi que la liste des erreurs La console affiche la sortie de l'application ainsi que la liste des erreurs

En fait, il y a deux entrees pour 816.json - une de notre experience d’evaluation d’expression, et l’autre mise la par le programme lui-meme, apres que nous avons corrige et reexecute la methode parse() .

Resume

Dans cet article, nous avons appris comment deboguer efficacement les exceptions Java en collectant un contexte supplementaire qui pointe vers la cause de l’echec. Ensuite, en utilisant les capacites du debogueur, nous avons pu annuler l’erreur, transformer la session de debogage en bac a sable pour tester une correction, et la livrer a l’execution en plein milieu du plantage, le tout sans redemarrer l’application.

Ces techniques simples peuvent etre tres puissantes et vous faire gagner beaucoup de temps dans certaines situations. Dans les prochains articles, nous discuterons de plus de conseils et astuces de debogage, alors restez a l’ecoute !

all posts ->