Panoramica

Il debug di un’applicazione Java in remoto può essere utile in più di un caso.

In questo tutorial, scopriremo come farlo usando gli strumenti di JDK.

L’applicazione

Iniziamo a scrivere un’applicazione. La eseguiremo su una postazione remota e la debuggeremo localmente attraverso questo articolo:

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

Il Java Debug Wire Protocol è un protocollo usato in Java per la comunicazione tra un debuggee e un debugger. Il debuggee è l’applicazione da sottoporre a debug mentre il debugger è un’applicazione o un processo che si connette all’applicazione da sottoporre a debug.

Entrambe le applicazioni girano sulla stessa macchina o su macchine diverse. Ci concentreremo su quest’ultima.

3.1. Opzioni di JDWP

Utilizzeremo JDWP negli argomenti della riga di comando della JVM quando lanciamo l’applicazione debuggee.

La sua invocazione richiede una lista di opzioni:

  • transport è l’unica opzione completamente richiesta. Definisce quale meccanismo di trasporto utilizzare. dt_shmem funziona solo su Windows e se entrambi i processi girano sulla stessa macchina, mentre dt_socket è compatibile con tutte le piattaforme e permette ai processi di girare su macchine diverse
  • server non è un’opzione obbligatoria. Questa bandiera, quando è attiva, definisce il modo in cui si attacca al debugger. O espone il processo attraverso l’indirizzo definito nell’opzione address. Altrimenti, JDWP ne espone uno di default
  • suspend definisce se la JVM deve sospendere e aspettare che un debugger si attacchi o meno
  • address è l’opzione contenente l’indirizzo, generalmente una porta, esposto dal debuggee. Può anche rappresentare un indirizzo tradotto come una stringa di caratteri (come javadebug se usiamo server=y senza fornire un indirizzo su Windows)

3.2. Comando di lancio

Iniziamo lanciando l’applicazione remota. Forniremo tutte le opzioni elencate in precedenza:

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

Fino a Java 5, l’argomento JVM runjdwp doveva essere usato insieme all’altra opzione debug:

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

Questo modo di usare JDWP è ancora supportato ma sarà abbandonato nelle versioni future. Preferiamo l’uso della notazione più recente quando possibile.

3.3. Da Java 9

Finalmente, una delle opzioni di JDWP è cambiata con il rilascio della versione 9 di Java. Si tratta di un cambiamento minore, poiché riguarda solo un’opzione, ma farà la differenza se stiamo cercando di eseguire il debug di un’applicazione remota.

Questo cambiamento ha un impatto sul modo in cui l’indirizzo si comporta per le applicazioni remote. La vecchia notazione address=8000 si applica solo a localhost. Per ottenere il vecchio comportamento, useremo un asterisco con due punti come prefisso per l’indirizzo (ad esempio address=*:8000).

Secondo la documentazione, questo non è sicuro e si raccomanda di specificare l’indirizzo IP del debugger quando possibile:

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

JDB: The Java Debugger

JDB, il Java Debugger, è uno strumento incluso nel JDK concepito per fornire un comodo client debugger dalla riga di comando.

Per lanciare JDB, useremo la modalità attach. Questa modalità attacca JDB a una JVM in esecuzione. Esistono altre modalità di esecuzione, come listen o run, ma sono per lo più convenienti quando si esegue il debug di un’applicazione in esecuzione locale:

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

4.1. Breakpoints

Continuiamo mettendo alcuni breakpoints nell’applicazione presentata nella sezione 1.

Imposteremo un breakpoint sul costruttore:

> stop in OurApplication.<init>

Ne imposteremo un altro nel metodo statico main, usando il nome completo della classe String:

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

Infine, imposteremo l’ultimo sul metodo di istanza buildInstanceString:

> stop in OurApplication.buildInstanceString(int)

Ora dovremmo notare che l’applicazione server si ferma e che nella nostra console di debugger viene stampato quanto segue:

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

Aggiungiamo ora un punto di interruzione su una linea specifica, quella dove viene stampata la variabile app.instanceString viene stampata:

> stop at OurApplication:7

Noi notiamo che at viene usato dopo stop invece di in quando il breakpoint è definito su una linea specifica.

4.2. Navigare e valutare

Ora che abbiamo impostato i nostri breakpoint, usiamo cont per continuare l’esecuzione del nostro thread fino a raggiungere il breakpoint sulla linea 7.

Dovremmo vedere quanto segue stampato nella console:

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

Come promemoria, ci siamo fermati sulla linea che contiene il seguente pezzo di codice:

System.out.println(app.instanceString);

Si sarebbe potuto fermare su questa linea anche fermandosi sul metodo main e digitando step due volte. step esegue la linea di codice corrente e ferma il debugger direttamente sulla linea successiva.

Ora che ci siamo fermati, il debugger sta valutando la nostra staticString, l’instanceString dell’app, la variabile locale i e infine sta dando un’occhiata a come valutare altre espressioni.

Stampiamo staticField nella console:

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

Mettiamo esplicitamente il nome della classe prima del campo statico.

Stampiamo ora il campo istanza di app:

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

Prossimo, vediamo la variabile i:

> print ii = 68741

A differenza delle altre variabili, le variabili locali non richiedono di specificare una classe o un’istanza. Possiamo anche vedere che print ha esattamente lo stesso comportamento di eval: entrambi valutano un’espressione o una variabile.

Valutiamo una nuova istanza di OurApplication per la quale abbiamo passato un intero come parametro del costruttore:

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

Ora che abbiamo valutato tutte le variabili necessarie, vogliamo cancellare i breakpoint impostati in precedenza e lasciare che il thread continui la sua elaborazione. Per ottenere questo, useremo il comando clear seguito dall’identificatore del punto di interruzione.

L’identificatore è esattamente lo stesso usato prima con il comando stop:

> clear OurApplication:7Removed: breakpoint OurApplication:7

Per verificare se il punto di interruzione è stato rimosso correttamente, useremo clear senza argomenti. Questo mostrerà la lista dei breakpoint esistenti senza quello che abbiamo appena cancellato:

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

Conclusione

I:n questo rapido articolo, abbiamo scoperto come usare JDWP insieme a JDB, entrambi strumenti del JDK.