Java: debug remoto con Visual Studio Code

Sullo stesso filone del precedente articolo sul debug di PHP con XDebug in un container Docker, in questo articolo mostrerò come configurare un container di Docker per consentire il debug passo-passo con Java.

All’alba dei tempi… ma ancora oggi…

Come per tutti i linguaggi di programmazione, Java espone dei metodi, principalmente utili agli sviluppatori ed ai tester per poter verificare il risultato di una computazione complessa o semplicemente per tenere sotto controllo un valore.

Questi metodi fanno parte della classe statica Out del package System. Il più utilizzato, credo sia println, almeno io ne faccio un uso importante e, cercando un po’ in giro, chiunque programmi in Java ed abbia detto la sua sul Web, l’ha menzionato almeno una volta.

Il metodo println, restituisce in output sullo Standard Output (aka STDOUT) di sistema il valore che abbiamo fornito in input alla funzione. Il metodo è implementato per accettare una moltitudine di tipi… e se il tipo non è contemplato, esiste il più generico metodo che accetta Object.

Quindi, se il software è in esecuzione da CLI e nel codice elaborato è presente l’istruzione che segue

System.out.println("Ciao Java!");

il risultato sarà trovarsi in console un fantastico Ciao Java!

In ogni software che si rispetti ci sarà, per intenzione o per dimenticanza, un println.

Ma a volte solo questo metodo non basta.

Il caso d’uso

Prendiamo come esempio questo spezzone di codice:

// ...
public String doSomething(String param1){
   String computedValue = doSomeComplexTask(param1);
   if( computedValue.equals("done") ){
      return "ok";
   } else {
      return doSomething(computedValue);
   }
}
// ...

Si tratta di una funzione ricorsiva, di per sé già complessa da comprendere per molti, ma ancora di più da debuggare direttamente da console, dovendo stampare una quantità considerevole di messaggi di output per verificare i valori, le condizioni ed il comportamento del codice nei vari contesti.

Il metodo svolge un task molto complesso al suo interno (doSomeComplexTask) e ritorna una stringa "ok" se il risultato della computazione è "done". In tutti gli altri casi richiama sè stessa (doSomething).

Metti caso che viene riscontrato un problema di output per un particolare valore che comporta la ricorsione con una nidificazione per migliaia di invocazioni. Il metodo System.out.println può essere di aiuto, ma l’output sarebbe così verboso da dover contornare il codice di tanti if, else ed armarci di tanta pazienza per analizzare dei log eccessivamente verbosi.

Il contesto

Come anticipato già in testa a questo articolo, si parla di Java in un container Docker.

Utilizzo Docker anche in ambiente locale, così da avere un sistema sufficientemente pulito ed avere numerosi vantaggi:

  1. nessun software, web server/application server o libreria particolare risulta installata nel mio computer;
  2. posso replicare i medesimi comportamenti di un ambiente di configurazione;
  3. posso velocemente verificare se il software richiede dei cambiamenti di impostazioni al cambio di versione di una sola dipendenza o dell’intero application server senza necessitare di installazioni che potrebbero alterare la configurazione precedente;
  4. posso condividere con i colleghi di Axio Studio, la configurazione dell’ambiente trasmettendo una manciata di file e anche loro sono in grado di ottenere i miei medesimi risultati.

Quale Application Server installare per Java?

Ce ne sono tanti. Da qualche anno va di moda Spring Boot. Quindi non spiegherò come configurare il remote debug con Spring Boot, potete trovare numerose guide a riguardo in giro per il Web e questo articolo non avrebbe alcun valore aggiunto.

In questo articolo spiegherò come attivare il debug in WildFly su JBoss.

Ma quale versione di WildFly? Quale versione di JBoss? Quale versione di Java?

In un mio precedente articolo ho spiegato come generare del codice per l’invocazione di un WebService XMLSoap attraverso il WSDL di riferimento, usando Java17. Continuerò quindi con questa versione. Tuttavia la versione di Java è pressoché ininfluente.

Considerando che a partire dalla versione 26 di WildFly è stato reso più articolato il processo di configurazione, ai fini illustrativi di questo articolo creerebbe solo dell’inutile rumore, quindi partirò dall’installazione della versione 25 dell’application server WidlFly.

Il container sarà tirato su attraverso docker compose che si avvale di un Dockerfile per la build dell’immagine opportunamente strutturato.

Il docker-compose.yml avrà questa forma:

services:
  example:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - 8080:8080
      - 8443:8443
      - 9990:9990
      - 9993:9993
    environment:
      - WILDFLY_PASSWORD=mysecret
      - WILDFLY_MANAGEMENT_LISTEN_ADDRESS=

Per una veloce analisi del file:

  • la build utilizza un file Dockerfile che si trova nella medesima cartella del docker-compose.yml
  • Le porte esposte sono le classiche:
    • 8080 per le richieste HTTP
    • 8443 per le richieste HTTPS
    • 9990 per la management console di WildFly, questa solitamente non viene esposta in ambienti di stage e produzione
    • 9993 che ha lo stesso scopo della 9990 ma su protocollo sicuro HTTPS.
  • Per consentire alla management console di essere avviata, sarà lasciata la variabile di sistema WILDFLY_MANAGEMENT_LISTEN_ADDRESS vuota e sarà definita la variabile WILDFLY_PASSWORD con una chiave sicura. Questo aspetto può essere ignorato ai fini dell’articolo.

Mentre il Dockerfile di riferimento sarà:

# PARTE 1
FROM alpine:3.18.4 as base
RUN apk add --no-cache unzip curl openjdk17 && \
    mkdir /wildfly
ADD https://github.com/wildfly/wildfly/releases/download/25.0.0.Final/wildfly-25.0.0.Final.zip /wildfly/wildfly.zip
WORKDIR /wildfly
RUN unzip wildfly.zip && \
    rm wildfly.zip && \
    mv * server

# PARTE 2
FROM maven:3.9.3-ibm-semeru-17-focal as build
RUN mkdir /source && \
    chmod 777 /source
WORKDIR /source
COPY . .
RUN mvn clean package -DskipTests war:war

# PARTE 3
FROM base
WORKDIR /wildfly/server/standalone/deployments/
COPY --from=build --chown=root:root /source/target/example.war example.war

EXPOSE 8080 8443 9990 9993

CMD ["/wildfly/server/bin/standalone.sh", "-b", "0.0.0.0", "-bmanagement", "0.0.0.0"]

Ho suddiviso il dockerfile in 3 parti per semplificarne la comprensione:

  1. Si predispone l’application server WildFly a partire da Linux Alpine, una versione minimale di linux che contiene a stento un package manager e i comandi essenziali da riga di comando. La prediligo perchè il layer è ridotto all’osso e la superficie di attacco di eventuali hacker è ridotta.
  2. Si compila il codice (il war finale avrà come nome example.war) con l’utilizzo di maven su un containe temporaneo
  3. Si installa l’artefatto nell’application server con nome example.war

Abilitare il debugger nel Dockerfile

L’attivazione del debugger è un’operazione alquanto semplice.

Sarà necessario indicare su quale porta si vuole attivare il remote debugger ed esporla.

Considerando il dockerfile precedente, basterà modificare giusto quanto basta la parte 3 del file, come segue:

...

#PARTE 3
FROM base
WORKDIR /wildfly/server/standalone/deployments/
COPY --from=build --chown=root:root /source/target/example.war example.war

EXPOSE 8080 8787 8443 9990 9993

CMD ["/wildfly/server/bin/standalone.sh", "--debug", "*:8787", "-b", "0.0.0.0", "-bmanagement", "0.0.0.0"]

Esporre le porte nel docker-compose.yml

Nel docker-compose.yml bisognerà esporre la porta 8787 ed impostare una variabile d’ambiente DEBUG_PORT che fa riferimento alla medesima porta.

Quindi la nuova struttura del file risulterà come segue:

services:
  example:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - 8080:8080
      - 8443:8443
      - 9990:9990
      - 9993:9993
      - 8787:8787
    environment:
      - DEBUG_PORT=8787
      - WILDFLY_PASSWORD=mysecret
      - WILDFLY_MANAGEMENT_LISTEN_ADDRESS=

In un contesto lavorativo, scrivere direttamente nel Dockerfile o nel docker-compose.yml le porte ed i valori non è un approccio corretto poichè la stessa configurazione potrebbe risultare inutilizzabile in un altro contesto.

Solitamente in Axio Studio preferiamo usare dei file di configurazione (.env) con tutte le variabili di ambiente che possono cambiare in virtù del contesto (sviluppo, stage, produzione) e del singolo computer dello sviluppatore che può cambiare le porte a seconda dei servizi già attivi.

Configurare Visual Studio Code per il debug remoto

Come già scritto in altre occasioni, in Axio Studio utilizziamo Visual Studio Code per lo sviluppo quotidiano poichè ci permette di operare con diversi linguaggi di programmazione, in diversi ambiti ed altrettanti contesti attraverso un unico strumento. Le indicazioni che riporterò di seguito sono applicabili ad altri IDE, sarà necessario solo collocare le medesime informazioni nell’interfaccia o nel file opportuno.

Se non è presente nel progetto (di default non è presente, ma non si sa mai se si eredita del codice da terzi), creare una nuova configurazione di debug di Visual Studio Code.

Può bastare cliccare sull’icona di debug nella sidebar dell’IDE e creare una nuova configurazione fornendo i seguenti valori:

{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "java",
            "name": "Debug Example",
            "projectName": "My Project",
            "request": "attach",
            "hostName": "localhost",
            "port": 8787
        }

    ]
}

mentre projectName e name sono informazioni puramente descrittive, è importante focalizzarsi sulle altre 4 chiavi:

  • type: deve essere "java", non c’è molto da spiegare.
  • request: deve essere "attach", il alcuni contesti potreste trovare “launch” ma serve per l’esecuzione del debugger con compilazione direttamente sulla macchina stessa nella quale c’è Visual Studio Code e un runtime di Java. Quindi si può sintetizzare in: deve essere "attach".
  • hostName: è il nome dell’host o l’indirizzo IP su cui è abilitato il debugger. Se Docker sta lavorando in abiente locale, sarà localhost, altrimenti è opportuno indicare l’indirizzo IP attraverso il quale l’istanza di Docker è raggiungibile.
  • port, è la porta su cui è esposto il debugger. Nel docker-compose.yml avendo indicato tale valore com 8787 (quella di default per il remote debugger), sarà necessario indicare per questa configurazione il medesimo valore.

Fatta la configurazione, il sistema è pronto al debug remoto del codice in Java passo-passo.

Avviare il servizio (docker compose build && docker compose up) ed il debugger (da Visual Studio Code tasto F5) et voilà!

Conclusioni

Anche questo articolo è arrivato a conclusione. In virtù del proprio ambiente di sviluppo o di test, potrebbe essere necessario modificare qualche parametro o le porte esposte dal servizio, tuttavia è possibile usare questa guida come base di riferimento per configurare il proprio contesto di debug. Ricordo che la configurazione tra diversi application server e tra versioni diverse dello stesso WildFly può cambiare.

Se vuoi condividere la tua esperienza, lascia un commento così da arricchire di dettagli questo tutorial.