Overzicht

Het debuggen van een Java Applicatie op afstand kan in meer dan één geval handig zijn.

In deze tutorial zullen we ontdekken hoe we dat kunnen doen met behulp van JDK’s tooling.

De Applicatie

Laten we beginnen met het schrijven van een applicatie. We zullen het uitvoeren op een externe locatie en debuggen het lokaal door middel van dit artikel:

public class OurApplication { private static String staticString = "Static String"; private String instanceString; public static void main(String args) { for (int i = 0; i < 1_000_000_000; i++) { OurApplication app = new OurApplication(i); System.out.println(app.instanceString); } } public OurApplication(int index) { this.instanceString = buildInstanceString(index); } public String buildInstanceString(int number) { return number + ". Instance String !"; }}

JDWP: The Java Debug Wire Protocol

The Java Debug Wire Protocol is een protocol dat wordt gebruikt in Java voor de communicatie tussen een debuggee en een debugger. De debuggee is de applicatie die wordt gedebugged, terwijl de debugger een applicatie of een proces is dat verbinding maakt met de applicatie die wordt gedebugged.

Beide applicaties draaien ofwel op dezelfde machine of op verschillende machines. We zullen ons richten op de laatste.

3.1. JDWP’s Options

We zullen JDWP gebruiken in de JVM command-line argumenten bij het starten van de debuggee applicatie.

Its aanroeping vereist een lijst van opties:

  • transport is de enige volledig vereiste optie. Het definieert welk transport mechanisme gebruikt moet worden. dt_shmem werkt alleen op Windows en als beide processen op dezelfde machine draaien terwijl dt_socket compatibel is met alle platforms en de processen op verschillende machines laat draaien
  • server is geen verplichte optie. Deze vlag, indien aan, definieert de manier waarop het zich verbindt met de debugger. Ofwel wordt het proces blootgesteld via het adres gedefinieerd in de adres optie. Anders geeft JDWP een standaard adres
  • suspend definieert of de JVM moet opschorten en wachten op een debugger of niet
  • address is de optie die het adres, meestal een poort, bevat dat door de debuggee wordt gebruikt. Het kan ook een adres vertalen als een tekenreeks (zoals javadebug als we server=y gebruiken zonder een adres te geven op Windows)

3.2. Launch Command

Laten we beginnen met het starten van de applicatie op afstand. We geven alle eerder genoemde opties:

java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8000 OurApplication

Tot Java 5 moest het JVM-argument runjdwp samen met de andere optie debug worden gebruikt:

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

Deze manier om JDWP te gebruiken wordt nog steeds ondersteund, maar zal in toekomstige versies worden afgeschaft. We zullen de voorkeur geven aan het gebruik van de nieuwere notatie wanneer mogelijk.

3.3. Sinds Java 9

Eindelijk is een van de opties van JDWP veranderd met de release van versie 9 van Java. Dit is een kleine wijziging omdat het maar om één optie gaat, maar het maakt wel verschil als we een applicatie op afstand proberen te debuggen.

Deze wijziging heeft invloed op de manier waarop het adres zich gedraagt voor applicaties op afstand. De oudere notatie address=8000 is alleen van toepassing op localhost. Om het oude gedrag te bereiken, gebruiken we een sterretje met een dubbele punt als voorvoegsel voor het adres (b.v. address=*:8000).

Volgens de documentatie is dit niet veilig en wordt aanbevolen om het IP-adres van de debugger op te geven wanneer dat mogelijk is:

java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=127.0.0.1:8000

JDB: The Java Debugger

JDB, de Java Debugger, is een programma dat in de JDK is opgenomen om een handige debugger-client vanaf de command-line te bieden.

Om JDB te starten, gebruiken we de attach mode. Deze mode koppelt JDB aan een draaiende JVM. Er bestaan ook andere modes, zoals listen of run, maar die zijn vooral handig bij het debuggen van een lokaal draaiende applicatie:

jdb -attach 127.0.0.1:8000> Initializing jdb ...

4.1. Breakpoints

Laten we verder gaan door een aantal breakpoints te plaatsen in de applicatie die in sectie 1 is gepresenteerd.

We zetten een breekpunt op de constructor:

> stop in OurApplication.<init>

We zetten een ander in de statische methode main, met behulp van de volledig gekwalificeerde naam van de klasse String:

> stop in OurApplication.main(java.lang.String)

Ten slotte zetten we de laatste op de instance-methode buildInstanceString:

> stop in OurApplication.buildInstanceString(int)

We zouden nu moeten zien dat de server applicatie stopt en dat het volgende wordt afgedrukt in onze debugger console:

> Breakpoint hit: "thread=main", OurApplication.<init>(), line=11 bci=0

Laten we nu een breekpunt toevoegen op een specifieke regel, de regel waar de variabele app.instanceString wordt afgedrukt:

> stop at OurApplication:7

We zien dat at wordt gebruikt na stop in plaats van in wanneer het breekpunt is gedefinieerd op een specifieke regel.

4.2. Navigeren en evalueren

Nu we onze breekpunten hebben ingesteld, laten we de uitvoering van onze thread voortzetten tot we het breekpunt op regel 7 bereiken.

We zouden het volgende in de console moeten zien verschijnen:

> Breakpoint hit: "thread=main", OurApplication.main(), line=7 bci=17

Als herinnering, we zijn gestopt op de regel die het volgende stuk code bevat:

System.out.println(app.instanceString);

Stoppen op deze regel had ook gekund door te stoppen op de main method en twee keer step in te typen. step voert de huidige regel code uit en stopt de debugger direct op de volgende regel.

Nu we gestopt zijn, evalueert de debugee onze staticString, de app’s instanceString, de lokale variabele i en kijkt tenslotte hoe andere expressies geëvalueerd kunnen worden.

Laten we staticField afdrukken naar de console:

> eval OurApplication.staticStringOurApplication.staticString = "Static String"

We zetten expliciet de naam van de klasse voor het static field.

Laten we nu het instance-veld van app afdrukken:

> eval app.instanceStringapp.instanceString = "68741. Instance String !"

Volgende, laten we de variabele i bekijken:

> print ii = 68741

In tegenstelling tot de andere variabelen, hoeven lokale variabelen geen klasse of instantie op te geven. We kunnen ook zien dat print precies hetzelfde gedrag heeft als eval: ze evalueren allebei een expressie of een variabele.

We evalueren een nieuwe instantie van OurApplication waarvoor we een integer als constructorparameter hebben doorgegeven:

> print new OurApplication(10).instanceStringnew OurApplication(10).instanceString = "10. Instance String !"

Nu we alle variabelen hebben geëvalueerd die we nodig hadden, willen we de eerder ingestelde breekpunten verwijderen en de thread verder laten gaan met zijn verwerking. Om dit te bereiken, gebruiken we het commando clear gevolgd door de identifier van het breekpunt.

De identifier is precies dezelfde als die eerder is gebruikt met het commando stop:

> clear OurApplication:7Removed: breakpoint OurApplication:7

Om te controleren of het breekpunt correct is verwijderd, gebruiken we clear zonder argumenten. Dit toont de lijst van bestaande onderbrekingspunten zonder degene die we zojuist hebben verwijderd:

> clearBreakpoints set: breakpoint OurApplication.<init> breakpoint OurApplication.buildInstanceString(int) breakpoint OurApplication.main(java.lang.String)

Conclusie

I:n dit korte artikel hebben we ontdekt hoe we JDWP samen met JDB kunnen gebruiken, beide JDK tools.