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 :
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 .
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() :
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.
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 :
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 :
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 :
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 :
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 apparait confirmant que la correction a ete chargee dans l’application en cours d’execution.
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 :
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 :
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 :
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.
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 !