Debug remoto dell'applicazione Java

1. Panoramica

Il debug di un'applicazione Java remota può essere utile in più di un caso.

In questo tutorial scopriremo come farlo utilizzando gli strumenti di JDK.

2. L'applicazione

Cominciamo scrivendo un'applicazione. Lo eseguiremo in una posizione remota e lo eseguiremo in locale tramite 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 !"; } } 

3. JDWP: il protocollo Java Debug Wire

Il Java Debug Wire Protocol è un protocollo utilizzato in Java per la comunicazione tra un debuggee e un debugger . Il debuggee è l'applicazione in fase di debug mentre il debugger è un'applicazione o un processo che si connette all'applicazione in fase di debug.

Entrambe le applicazioni vengono eseguite sulla stessa macchina o su macchine diverse. Ci concentreremo su quest'ultimo.

3.1. Opzioni di JDWP

Useremo JDWP negli argomenti della riga di comando JVM quando avvieremo l'applicazione oggetto del debug.

La sua chiamata richiede un elenco di opzioni:

  • il trasporto è l'unica opzione completamente richiesta. Definisce quale meccanismo di trasporto utilizzare. dt_shmem funziona solo su Windows e se entrambi i processi vengono eseguiti sulla stessa macchina mentre dt_socket è compatibile con tutte le piattaforme e consente l'esecuzione dei processi su macchine diverse
  • il server non è un'opzione obbligatoria. Questo flag, se attivato, definisce il modo in cui si collega al debugger. Espone il processo tramite l'indirizzo definito nell'opzione indirizzo . Altrimenti, JDWP ne espone uno predefinito
  • suspend definisce se la JVM deve sospendere e attendere che un debugger si colleghi o meno
  • indirizzo è 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. Avvia comando

Cominciamo avviando 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 utilizzato insieme all'altra opzione di debug :

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

Questo modo di utilizzare JDWP è ancora supportato ma verrà abbandonato nelle versioni future. Preferiremo l'uso della notazione più recente quando possibile.

3.3. Poiché Java 9

Infine, una delle opzioni di JDWP è cambiata con il rilascio della versione 9 di Java. Questa è una modifica piuttosto minore poiché riguarda solo un'opzione, ma farà la differenza se stiamo cercando di eseguire il debug di un'applicazione remota.

Questa modifica influisce sul comportamento dell'indirizzo 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 es. Address = *: 8000 ).

Secondo la documentazione, questo non è sicuro e si consiglia 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

4. JDB: il debugger Java

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

Per avviare JDB, useremo la modalità di collegamento . Questa modalità collega JDB a una JVM in esecuzione. Esistono altre modalità di esecuzione, come ascolta o esegui, ma sono per lo più convenienti durante il debug di un'applicazione in esecuzione localmente:

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

4.1. Punti di interruzione

Continuiamo inserendo alcuni punti di interruzione nell'applicazione presentata nella sezione 1.

Imposteremo un punto di interruzione sul costruttore:

> stop in OurApplication. 

Ne imposteremo un altro nel metodo statico main , utilizzando 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 l'arresto dell'applicazione server e la stampa di quanto segue nella nostra console del debugger:

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

Aggiungiamo ora un punto di interruzione su una riga specifica, quella in cui viene stampata la variabile app.instanceString :

> stop at OurApplication:7 

Notiamo che at viene utilizzato dopo stop anziché in quando il punto di interruzione è definito su una riga specifica.

4.2. Naviga e valuta

Ora che abbiamo impostato i nostri punti di interruzione, usiamo cont per continuare l'esecuzione del nostro thread fino a raggiungere il punto di interruzione sulla riga 7.

Dovremmo vedere quanto segue stampato nella console:

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

Come promemoria, ci siamo fermati sulla riga contenente la seguente parte di codice:

System.out.println(app.instanceString); 

Stopping on this line could have also been done by stopping on the main method and typing step twice. step executes the current line of code and stops the debugger directly on the next line.

Now that we've stopped, the debugee is evaluating our staticString, the app‘s instanceString, the local variable i and finally taking a look at how to evaluate other expressions.

Let's print staticField to the console:

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

We explicitly put the name of the class before the static field.

Let's now print the instance field of app:

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

Next, let's see the variable i:

> print i i = 68741 

Unlike the other variables, local variables don't require to specify a class or an instance. We can also see that print has exactly the same behavior as eval: they both evaluate an expression or a variable.

We'll evaluate a new instance of OurApplication for which we've passed an integer as a constructor parameter:

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

Now that we've evaluated all the variables we needed to, we'll want to delete the breakpoints set earlier and let the thread continue its processing. To achieve this, we'll use the command clear followed by the breakpoint's identifier.

The identifier is exactly the same as the one used earlier with the command stop:

> clear OurApplication:7 Removed: breakpoint OurApplication:7 

Per verificare se il punto di interruzione è stato rimosso correttamente, useremo clear senza argomenti. Questo mostrerà l'elenco dei punti di interruzione esistenti senza quello che abbiamo appena eliminato:

> clear Breakpoints set: breakpoint OurApplication. breakpoint OurApplication.buildInstanceString(int) breakpoint OurApplication.main(java.lang.String[]) 

5. conclusione

In questo rapido articolo, abbiamo scoperto come utilizzare JDWP insieme a JDB, entrambi gli strumenti JDK.

Ulteriori informazioni sugli strumenti possono, ovviamente, essere trovate nei rispettivi riferimenti: JDWP e JDB - per approfondire gli strumenti.