Erweiterte IntelliJ-Debuggerfunktionen, die Sie noch nicht kennen
30. Januar 2023
0 Min. LesezeitKürzlich habe ich mein Debugging-Buch und einen Debugging-Kurs fertiggestellt. Seitdem werde ich häufig nach meinen bevorzugten Debugging-Funktionen gefragt. Debugging umfasst weit mehr als den IDE-Debugger. Tatsächlich behandelt nur das erste Kapitel des Buches diesen Aspekt. Wenn wir jedoch an Debugging denken, schweifen unsere Gedanken zur IDE ab. Dabei gibt es in diesen großartigen Tools noch viele Ecken und Winkel zu entdecken.
Der Hauptgrund dafür ist einfach: Wir haben nie gelernt, zu debuggen. Debugging-Kenntnisse lassen sich nicht testen, deshalb wird es an Universitäten nicht gelehrt.
Wir lernen es im Laufe der Arbeit – und das ist schrecklich. Kein Wunder, dass manche Entwickler Debugging behandeln wie den Gang zum Müllcontainer: Sie halten sich die Nase zu und laufen zur Tür, um es so schnell wie möglich hinter sich zu bringen. Debugging ist mehr als die Summe seiner Teile. Debugger sind fantastische Tools, mit denen wir tiefe Einblicke in unsere Arbeit gewinnen. Sie machen das Innenleben einer Anwendung auf völlig neue Weise sichtbar und verschaffen uns Einblicke, die in keinem anderen Handwerk möglich sind.
In diesem Beitrag stellen wir drei Debuggerfunktionen vor, die bei meinen Vorträgen zu diesem Thema besonders gut beim Publikum ankommen. Diese Funktionen sollten zwar mit den meisten JetBrains-IDEs funktionieren, aber leider sind sie alle drei in VS Code nicht verfügbar. Ich glaube, das war eine bewusste Entscheidung des VS-Code-Teams, um die Umgebung zu vereinfachen – eine Entscheidung, die das Team meiner Meinung nach überdenken sollte.
Drei IntelliJ-Debuggingfunktionen:
Debug-Konfiguration mit Markierungsobjekten
Darstellungen von Einträgen
Speicherüberwachung
IntelliJ-Debug-Konfiguration mit Markierungsobjekten
Eine der größten Herausforderungen beim Debugging ist es, den Überblick über die eigene Arbeit zu behalten. Wir halten an einem Breakpoint an und sehen ein Objekt. Ist es dasselbe Objekt wie zuvor? Hat es dieselbe Referenz, dieselben Werte, dieselbe Identität, dieselbe Zeigeradresse?
Früher habe ich Zeigeradressen auf einen Zettel geschrieben, um den Überblick über bereits untersuchte Objekte zu behalten und zu prüfen, ob es sich nicht um eine neue Adresse handelte. Das ist frustrierend und verwirrend. Ich kann ja nicht einmal meine eigene Handschrift lesen … Das muss doch besser gehen?
Das geht. Wenn wir im Debugger ein Objekt untersuchen, können wir es über das Kontextmenü markieren, statt den Wert aufzuschreiben.

Sobald wir diese Option auswählen, werden wir aufgefordert, dem Objekt einen Namen zu geben. Dieser Dialog ist verwirrend. Sie könnten fälschlicherweise annehmen, dass es sich dabei lediglich um einen Alias für den Watch-Eintrag handelt. Das ist nicht der Fall.

Tatsächlich definieren wir hier eine neue globale Variable. Wir können den Wert im Watch-Fenster unabhängig vom Gültigkeitsbereich untersuchen (was schon ziemlich praktisch ist), aber wir können ihn auch in Bedingungen verwenden. So hält der Breakpoint nur an, wenn sich der Wert ändert – wie hier zu sehen.

Das ist ausgesprochen leistungsstark. Wir können damit Threading-Probleme erkennen, indem wir den aktuellen Thread markieren. Außerdem können Sie damit Objekte mit derselben Identität, aber unterschiedlichen Zeigern im Blick behalten. Ein gutes Beispiel ist objektrelationales Mapping, bei dem möglicherweise zwei Instanzen derselben Entität gleichzeitig vorhanden sind. Solche Fehler lassen sich nur sehr schwer aufspüren.
Darstellungen von Einträgen
Der Watch-Bereich zählt zu den großartigsten Funktionen moderner IDEs. Während wir den Code schrittweise durchlaufen, liefert er uns auf einen Blick alle benötigten Informationen. Manchmal gleicht dieser „Blick“ jedoch eher einer Schnitzeljagd: Wir müssen Variablen aufklappen und uns durch komplexe Hierarchien arbeiten, nur um einen bestimmten Variablenwert zu finden.
In Java besteht eine gängige Behelfslösung darin, toString() zu überschreiben und einen aussagekräftigeren Wert zurückzugeben. Es gibt jedoch mehrere Gründe, warum ich das möglicherweise nicht möchte:
Möglicherweise gehört das Objekt nicht mir, sondern ist im Framework definiert.
Vielleicht ist das eine Anpassung, die nur für mich relevant ist. Ich möchte
toString()nicht für alle ändern.toString()ist wichtig und kann sich erheblich auf die Anwendungsleistung auswirken (zum Beispiel durch die Kosten der Protokollierung).toString()ist zu einfach. Was, wenn ich eine Variable untersuchen möchte, die eine Liste von Elementen enthält?
Mit Renderern können wir die Vorteile des Watch-Bereichs nutzen, ohne diese Nachteile in Kauf nehmen zu müssen. Ich demonstriere das anhand einer leicht angepassten Version der Spring-Pet-Clinic-Demo. Die Demo nutzt die Java Persistence API (JPA) zur Datenspeicherung. Spring Data bietet das Konzept eines Repositorys. Ein Repository lässt sich als Datenbanktabelle verstehen. Das ist nicht ganz exakt, da ein Repository eine umfassendere Abstraktion darstellt, aber im Allgemeinen ist es eine brauchbare Annäherung.
Wenn wir an einem Breakpoint anhalten, sehen wir normalerweise das nutzlose Durcheinander im folgenden Bild. visitRepository ist ein Repository, liefert mir beim Debugging jedoch keine nützlichen Informationen. Wie viele Elemente enthält die Tabelle? Welche Werte haben sie?
Ich kann den Watch-Eintrag weiter aufklappen, aber das bringt mich nicht weiter: Dieses Proxy-Objekt liefert keine nützlichen Informationen im Watch-Bereich.

Mit Renderern geht es besser. Klicken Sie zunächst mit der rechten Maustaste auf den Watch-Bereich und wählen Sie Datenansichten anpassen.

Im folgenden Dialog kann ich die Elemente meines Renderers festlegen und so ein wesentlich nützlicheres Ergebnis erzielen. Im Watch-Bereich sehen wir jetzt auf einen Blick die Anzahl der Elemente und können uns bei Bedarf jedes Element einzeln ansehen.

Links sehen Sie, wie das funktioniert. Wir passen das JPARepository an. Beachten Sie, dass perRepository im Watch-Bereich ein anderer Repository-Typ ist und deshalb nicht davon betroffen war. Als Nächstes legen wir den entsprechenden Code fest, der für die Darstellung ausgeführt wird. Rufen Sie für das Objekt die Methode count() auf, um die Anzahl der Objekte zu ermitteln, und erstellen Sie einen String, der im Watch-Bereich angezeigt wird.
Der zweite Teil ist sogar noch besser. Mit findAll() können wir die Elemente aus dem Repository abrufen und anzeigen, wenn das Objekt aufgeklappt wird. Im letzten Teil wird festgelegt, ob neben dem Repository ein Pluszeichen angezeigt werden soll, über das es aufgeklappt werden kann.
Das ist großartig. Es gibt jedoch einen entscheidenden Nachteil: Wir müssen das für jeden von uns definierten Objekttyp machen. Das ist mühsam … Oder etwa nicht?
Wir können Annotationen definieren, die diese Konfigurationen abbilden, und Lösungen wie diese direkt in unsere Bibliotheksdistribution integrieren. Dadurch wird das Debugging von Bibliotheken von Drittanbietern viel einfacher.
Speicherüberwachung
Wenn wir über Speicher sprechen, denken wir an Profiling-Tools. Wie ließe sich Speicher besser überwachen?

Profiler sind großartig, keine Frage. Aber sie sind breit angelegte Instrumente. Wenn wir einem Fehler ganz gezielt auf den Grund gehen, fehlt ihnen eine Lösung für die letzten Schritte. Außerdem eignen sie sich nicht dazu, ein einzelnes problematisches Objekt zu verfolgen oder einen detaillierten Blick auf das System zu werfen. Wir können beispielsweise die Speicheransicht aktivieren, indem wir oben rechts im Watch-Bereich klicken und „Speicher“ auswählen.

Anschließend sehen wir eine Liste der Objekte im Speicher und können auf jedes Objekt doppelklicken, um eine vollständige Liste der darin enthaltenen Objekte aufzurufen. In der Spalte „Diff“ sehen wir außerdem die Anzahl der Objekte jedes Typs, die zwischen diesem und dem letzten Breakpoint erstellt wurden.

Das allein ist schon beeindruckend, aber auch begrenzt. Wir wissen nicht, welche Objekte zu einem bestimmten Zeitpunkt erstellt wurden. Dafür müsste die IDE jede Objekterstellung jedes Typs verfolgen und auf jedes jemals erstellte Objekt eine Referenz behalten. Das ist nicht praktikabel. Wir können jedoch einen bestimmten Objekttyp auswählen, mit der rechten Maustaste darauf klicken und die Option Neue Instanzen verfolgen wählen.
Im obigen Screenshot habe ich das für java.lang.String getan. Das Watch-Symbol daneben zeigt dies an. So sehen wir die konkreten Objekte, die zwischen zwei Breakpoints erstellt wurden. Das ist äußerst nützlich, weil wir dadurch Einblicke in die internen Abläufe unseres Systemcodes gewinnen. Aber es gibt noch mehr …
Anschließend können wir für jede Objekterstellung auch den vollständigen Stacktrace abrufen und sehen, welche Zeilen sie ausgelöst haben. So verstehen wir jede Speicherbelegung und die Gründe dafür. Das hilft uns, die internen Abläufe großer, unbekannter Systeme nachzuvollziehen.
Die perfekte Ergänzung zu Ihrem Profiler.
Zum Schluss
Ich hoffe, dieser Beitrag hat den gewünschten WOW-Effekt erzielt. Entwickler begegnen dem Debugging meiner Meinung nach mit unbegründeter Scheu. Das liegt zu einem großen Teil an fehlenden Tools und Prozessen sowie mangelnder Unterstützung durch erfahrene Kollegen.
Debugging sollte eine befreiende Erfahrung sein. Es erdet uns, und beim Debugging fühlen wir uns alle wie Anfänger – doch das ist nichts Schlechtes. Trotz aller Herausforderungen sollte der Prozess Spaß machen und spannend sein. Wir sollten die verfügbaren Tools schätzen und sie voll ausschöpfen.
Behandeln Sie Ihren nächsten Bug nicht wie einen Fahrerflucht-Fall. Betrachten Sie ihn als Abenteuer. Ein Abenteuer, bei dem Sie die neuen Spielzeuge aus der Garage holen und den Bug mit Ihren neuen Fähigkeiten angehen.
