Java Stack Trace: Lesen und Verstehen zur Fehlerbehebung im Code

May 23, 2019
Autor:in:

Java Stack Trace: Lesen und Verstehen zur Fehlerbehebung im Code


Hallo und Danke fürs Lesen! Dieser Blogpost ist eine Übersetzung von Java Stack Trace: How to Read and Understand to Debug Code. Während wir unsere Übersetzungsprozesse verbessern, würden wir uns über Dein Feedback an help@twilio.com freuen, solltest Du etwas bemerken, was falsch übersetzt wurde. Wir bedanken uns für hilfreiche Beiträge mit Twilio Swag :)

Wenn beim Ausführen von Java-Anwendungen etwas schiefläuft, macht sich das meist zuerst durch Zeilen auf dem Bildschirm bemerkbar, die den folgenden ähneln. Dies ist ein Java Stack Trace und in diesem Post erkläre ich, was es damit auf sich hat, wie es entsteht und wie es gelesen und interpretiert wird. Viel zu kompliziert? Dann lies mal weiter...

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)

Java Stack Trace: Was ist das und wie funktioniert‘s?

Ein Stacktrace (auch bekannt als Stapelrückverfolgung) ist ein unfassbar praktisches Tool zum Debuggen von Code. Erfahre mehr darüber, was das ist und wie es funktioniert.

Was ist ein Java Stack Trace?

Ein Stacktrace zeigt den Aufrufstapel (ein Satz aktiver Stack-Frames) an und gibt Informationen über die Methoden, die der Code abruft. Normalerweise wird ein Stacktrace angezeigt, wenn eine Ausnahme im Code nicht korrekt verarbeitet wird. (Eine Ausnahme nutzt eine Runtime-Umgebung, um mitzuteilen, dass sich im Code ein Fehler befindet.) Dies kann einer der integrierten Ausnahmetypen oder eine benutzerdefinierte Ausnahme sein, die von einem Programm oder einer Bibliothek erstellt wurde.

Der Stacktrace enthält den Ausnahmetyp, eine Meldung sowie eine Liste aller Methodenaufrufe, die ausgeführt wurden, als die Ausnahme ausgelöst wurde.

Lesen von Java Stack Trace

Nehmen wir diesen Stacktrace mal auseinander. Die ersten Zeilen informieren uns über die Ausnahmen:

Beispiel: Java Stack Trace

Das ist ein guter Anfang. Zeile 2 zeigt an, welcher Code ausgeführt wurde, als die Ausnahme ausgelöst wurde:

Beispiel: Java Stack Trace

Das hilft, um das Problem einzugrenzen, doch welcher Teil des Codes rief badMethod auf? Die Antwort erhalten wir eine Zeile tiefer, die genauso gelesen wird. Und wie sind wir dorthin gelangt? Schauen wir uns die nächste Zeile an. So machen wir weiter, bis wir in der letzten Zeile angekommen sind, die der main-Methode der Anwendung entspricht. Lesen wir den Stacktrace von unten nach oben, können wir den genauen Pfad vom Beginn des Codes bis zur Ausnahme verfolgen.

Was war der Auslöser für das Stacktrace?

Ausnahme-Meme

Eine Ausnahme wird meist von einer expliziten throw-Aussage verursacht. Sieh dir den Dateinahmen und die Zeilennummer an, um zu prüfen, was die Ausnahme ausgelöst hat. Das sieht dann wahrscheinlich ungefähr so aus:

   throw new RuntimeException("Something has gone wrong, aborting!");

Das ist eine ideale Stelle, um mit der Suche nach dem zugrundeliegenden Problem zu beginnen: gibt es if-Anweisungen? Was macht dieser Code? Woher stammen die Daten, die in dieser Methode verwendet werden?

Es ist für Code auch möglich, ohne throw-Aussage eine Ausnahme auszulösen, hier ein Bespiel:

  • NullPointerException wenn obj null in Code ist, der obj.someMethod() ausruft
  • ArithmeticException, wenn in Integer-Arithmetik durch Null dividiert wird, z. B. 1/0 – interessanterweise gibt es keine Ausnahme, wenn es sich um eine Fließkommaberechnung handelt, denn dann wird bei 1.0/0.0 einfach nur infinity alles super! ausgegeben.
  • NullPointerException wenn ein Integer null in int in Code wie diesem entpackt wird: Integer a=null; a++;
  • Es gibt weitere Beispiele in der Spezifikation zur Java-Programmiersprache. Uns sollte also bewusst sein, dass Ausnahmen auch ohne expliziten Auslöser eintreten können.

Von Bibliotheken ausgelöste Ausnahmen

Eine der größten Stärken von Java sind die zahlreichen verfügbaren Bibliotheken. Jede beliebte Bibliothek wird gründlich getestet, daher ist es meist am besten, bei einer aus der Bibliothek stammenden Ausnahme zunächst zu prüfen, ob der Fehler auf die Verwendungsart des Codes zurückzuführen ist.

Nutzen wir beispielsweise die Klasse Fraction aus Apache Commons Lang und übergeben ein paar Eingaben wie diese:

    Fraction.getFraction(numberOfFoos, numberOfBars);

und numberOfBars ist null, dann sieht der Stacktrace folgendermaßen aus:

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)

Viele gute Bibliotheken bieten Javadoc mit Informationen über die Ausnahmeart, die ausgelöst wird, und den Grund des Auslösers. In diesem Fall dokumentiert Fraction.getFraction, dass eine ArithmeticException ausgelöst wird, wenn ein Fraction einen Null-Nenner aufweist. Hier sehen wir das auch ganz klar in der Meldung, bei komplizierteren oder unklaren Situationen sind die Dokumentationen jedoch eine große, zusätzliche Hilfe.

Zum Lesen dieses Stacktrace beginnen wir oben mit dem Ausnahmetyp – ArithmeticException und der Meldung The denominator must not be zero. Wir können uns nun vorstellen, was falschgelaufen ist, aber um zu erkennen, welcher Code die Ausnahme auslöste, müssen wir im Stacktrace weiter unten suchen und uns das Paket com.myproject (hier in der dritten Zeile) ansehen und das Zeilenende betrachten, wo der Code (MyProject.java:17) aussagt. Diese Zeile enthält Code, der Fraction.getFraction ausruft. Hier beginnen unsere Ermittlungen: Was wird an getFraction weitergegeben? Mit welchem Ursprung?

Bei großen Projekten mit vielen Bibliotheken können die Stacktraces aus Hunderten von Zeilen bestehen. Wenn du also einen großen Stacktrace siehst, dann übe, die Liste at ... at ... at ... zu scannen und nach deinem eigenen Code zu suchen. Das ist eine gute Chance, sich weiterzuentwickeln.

Best Practice: Aufgreifen und erneutes Auslösen von Ausnahmen

Stellen wir uns vor, wir arbeiten an einem großen Projekt, bei dem es um fiktive FooBars geht, und unser Code soll von anderen verwendet werden. Wir wollen den ArithmeticException aus Fraction aufgreifen und als etwas Projektspezifisches erneut auslösen. Das sieht dann so aus:

   try {
            ....
           Fraction.getFraction(x,y);
            ....
   } catch ( ArithmeticException e ){
           throw new MyProjectFooBarException("The number of FooBars cannot be zero", e);
   }

Das Aufgreifen und das erneute Auslösen von ArithmeticException hat ein paar Vorteile:

  • Unsere Nutzer müssen sich keine Gedanken um ArithmeticException machen. Daher sind wir flexibel genug, um die Verwendung von Commons-Lang zu ändern.
  • Wir können mehr Kontext hinzufügen, indem wir z. B. erklären, dass die Anzahl der FooBars das Problem auslöst.
  • Außerdem wird das Lesen von Stacktraces erleichtert, wie wir unten sehen können.

Wir müssen nicht alle Ausnahmen aufgreifen und erneut auslösen, aber wo in den Code-Ebenen gesprungen wird, z. B. bei Aufrufen in Bibliotheken, ist es durchaus sinnvoll.

Beachte, dass der Constructor für MyProjectFooBarException 2 Argumente einfügte: eine Meldung und eine verursachende Ausnahme. Alle Ausnahmen in Java haben ein cause-Feld. Beim Aufgreifen und erneutem Auslösen wie hier müssen wir dieses immer nutzen, um das Debuggen von Fehlern zu erleichtern. Ein Stacktrace sieht dann eventuell so aus:

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: The denominator must not be zero
    at org.apache.commons.lang3.math.Fraction.getFraction(Fraction.java:143)
    at com.myproject.module.MyProject.anotherMethod(MyProject.java:17)
    ... 2 more

Die zuletzt ausgelöste Ausnahme steht in der ersten Zeile, der Ort der Auslösung steht noch immer in der zweiten Zeile. Dennoch kann diese Art von Stacktrace für Verwirrung sorgen, denn das Aufgreifen und erneut Auslösen hat die Reihenfolge der Methodenaufrufe im Vergleich zu den vorherigen Stacktraces geändert. Die Hauptmethode steht nicht mehr ganz unten und der Code, der zuerst eine Ausnahme auslöste, steht nicht mehr ganz oben. Bei mehreren Aktionen mit Aufgreifen und erneut Auslösen von Ausnahmen wird das Ganze umfangreicher, aber das Prinzip bleibt:

Sieh dir die Abschnitte komplett an und suche nach dem Code, lies dann die relevanten Abschnitte von unten nach oben.

Bibliotheken im Vergleich zu Frameworks

Der Unterschied zwischen einer Bibliothek und einem Framework in Java ist Folgender:

  • Der Code ruft Methoden in einer Bibliothek auf.
  • Der Code wird von Methoden in einem Framework aufgerufen.

Ein gängiger Framework-Typ ist ein Webanwendungs-Server wie SparkJava oder Spring Boot. Verwenden wir SparkJava und Commons-Lang in unserem Code, sieht ein Stacktrace so aus:

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

Okay, das wird jetzt ziemlich lang. Wie zuvor sollten wir zunächst den eigenen Code in Verdacht haben, dennoch wird es immer schwieriger herauszufinden, wo dieser steckt. Oben sehen wir eine Framework-Ausnahme, unten die Bibliotheks-Ausnahme und genau in der Mitte versteckt sich der eigene Code. Ein Glück, da ist er ja!

Ein komplexer Framework oder eine komplexe Bibliothek kann mindestens ein Duzend Caused by:-Abschnitte erstellen, daher ist es gut, seinen eigenen Code zu suchen: Caused by: com.myproject... Lies dann den Abschnitt genauer und isoliere das Problem.

Einmal Java-Stacktrace zum Mitnehmen, bitte

Wenn du einmal verstanden hast, wie du Stacktraces lesen und verstehen kannst, findest du Probleme schneller und das Debuggen ist nicht mehr so anstrengend. Hier gilt: Übung macht den Meister. Also, keine Angst vor großen Stacktraces, es steckt eine Menge nützlicher Informationen darin, wenn du weißt, wie du diese extrahierst.

Level Up

Wenn du Tipps und Tricks zum Umgang mit Java-Stacktraces teilen möchtest, freue ich mich, wenn du dich mit mir dazu in Verbindung setzt.

mgilliard@twilio.com