Depuración remota de aplicaciones Java

1. Información general

La depuración de una aplicación Java remota puede resultar útil en más de un caso.

En este tutorial, descubriremos cómo hacerlo usando las herramientas de JDK.

2. La aplicación

Comencemos escribiendo una aplicación. Lo ejecutaremos en una ubicación remota y lo depuraremos localmente a través de este artículo:

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: el protocolo Java Debug Wire

El protocolo Java Debug Wire es un protocolo utilizado en Java para la comunicación entre un depurador y un depurador . El depurador es la aplicación que se está depurando, mientras que el depurador es una aplicación o un proceso que se conecta a la aplicación que se está depurando.

Ambas aplicaciones se ejecutan en la misma máquina o en máquinas diferentes. Nos centraremos en lo último.

3.1. Opciones de JDWP

Usaremos JDWP en los argumentos de la línea de comandos de JVM al iniciar la aplicación de depuración.

Su invocación requiere una lista de opciones:

  • el transporte es la única opción totalmente necesaria. Define qué mecanismo de transporte utilizar. dt_shmem solo funciona en Windows y si ambos procesos se ejecutan en la misma máquina, mientras que dt_socket es compatible con todas las plataformas y permite que los procesos se ejecuten en diferentes máquinas
  • servidor no es una opción obligatoria. Esta bandera, cuando está activada, define la forma en que se adjunta al depurador. O bien expone el proceso a través de la dirección definida en la opción de dirección . De lo contrario, JDWP expone uno predeterminado
  • suspender define si la JVM debe suspender y esperar a que un depurador se adjunte o no
  • dirección es la opción que contiene la dirección, generalmente un puerto, expuesta por el depurador. También puede representar una dirección traducida como una cadena de caracteres (como javadebug si usamos server = y sin proporcionar una dirección en Windows)

3.2. Iniciar comando

Comencemos lanzando la aplicación remota. Proporcionaremos todas las opciones enumeradas anteriormente:

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

Hasta Java 5, el argumento de JVM runjdwp tenía que usarse junto con la otra opción debug :

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

Esta forma de usar JDWP todavía es compatible, pero se eliminará en versiones futuras. Preferiremos el uso de la notación más nueva cuando sea posible.

3.3. Desde Java 9

Finalmente, una de las opciones de JDWP ha cambiado con el lanzamiento de la versión 9 de Java. Este es un cambio bastante menor, ya que solo se trata de una opción, pero marcará la diferencia si intentamos depurar una aplicación remota.

Este cambio afecta la forma en que se comporta la dirección para las aplicaciones remotas. La dirección de notación anterior = 8000 solo se aplica a localhost . Para lograr el comportamiento anterior, usaremos un asterisco con dos puntos como prefijo para la dirección (por ejemplo, dirección = *: 8000 ).

Según la documentación, esto no es seguro y se recomienda especificar la dirección IP del depurador siempre que sea posible:

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

4. JDB: el depurador de Java

JDB, el depurador de Java, es una herramienta incluida en el JDK concebida para proporcionar un cliente depurador conveniente desde la línea de comandos.

Para iniciar JDB, usaremos el modo adjuntar . Este modo conecta JDB a una JVM en ejecución. Existen otros modos de ejecución, como escuchar o ejecutar, pero son más convenientes al depurar una aplicación que se ejecuta localmente:

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

4.1. Puntos de interrupción

Continuemos poniendo algunos puntos de interrupción en la aplicación presentada en la sección 1.

Estableceremos un punto de interrupción en el constructor:

> stop in OurApplication. 

Estableceremos otro en el método estático main , usando el nombre completo de la clase String :

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

Finalmente, configuraremos el último en el método de instancia buildInstanceString :

> stop in OurApplication.buildInstanceString(int) 

Ahora deberíamos notar que la aplicación del servidor se detiene y lo siguiente se imprime en nuestra consola de depuración:

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

Agreguemos ahora un punto de interrupción en una línea específica, en la que se imprime la variable app.instanceString :

> stop at OurApplication:7 

Observamos que at se usa después de la parada en lugar de in cuando el punto de interrupción se define en una línea específica.

4.2. Navegar y evaluar

Ahora que hemos establecido nuestros puntos de interrupción, usemos cont para continuar la ejecución de nuestro hilo hasta que alcancemos el punto de interrupción en la línea 7.

Deberíamos ver lo siguiente impreso en la consola:

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

Como recordatorio, nos hemos detenido en la línea que contiene el siguiente código:

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 

Para verificar si el punto de interrupción se ha eliminado correctamente, usaremos clear sin argumentos. Esto mostrará la lista de puntos de interrupción existentes sin el que acabamos de eliminar:

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

5. Conclusión

En este artículo rápido, hemos descubierto cómo usar JDWP junto con JDB, ambas herramientas JDK.

Por supuesto, puede encontrar más información sobre las herramientas en sus respectivas referencias: JDWP y JDB, para profundizar en las herramientas.