Resumen

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

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

La aplicación

Comencemos escribiendo una aplicación. La ejecutaremos en una ubicación remota y la 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 !"; }}

JDWP: The Java Debug Wire Protocol

El Java Debug Wire Protocol es un protocolo utilizado en Java para la comunicación entre un debuggee y un debugger. El debuggee es la aplicación que se está depurando mientras que el debugger 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 esta última.

3.1. Opciones de JDWP

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

Su invocación requiere una lista de opciones:

  • transporte es la única opción totalmente necesaria. Define qué mecanismo de transporte utilizar. dt_shmem sólo 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 conecta 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 por defecto
  • 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. Comando de lanzamiento

Empecemos por lanzar la aplicación remota. Proporcionaremos todas las opciones listadas anteriormente:

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

Hasta Java 5, el argumento de la 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 está soportada pero se abandonará en futuras versiones. Preferiremos el uso de la notación más reciente 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. Se trata de un cambio bastante menor, ya que sólo afecta a una opción, pero supondrá una diferencia si intentamos depurar una aplicación remota.

Este cambio afecta a la forma en que se comporta la dirección para las aplicaciones remotas. La antigua notación address=8000 sólo se aplica a localhost. Para lograr el comportamiento antiguo, usaremos un asterisco con dos puntos como prefijo para la dirección (por ejemplo, address=*: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

JDB: The Java Debugger

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

Para lanzar JDB, utilizaremos el modo attach. Este modo adjunta JDB a una JVM en ejecución. Existen otros modos de ejecución, como listen o run, pero son más convenientes cuando se depura una aplicación que se ejecuta localmente:

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

4.1. Puntos de ruptura

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

Pondremos un punto de ruptura en el constructor:

> stop in OurApplication.<init>

Pondremos otro en el método estático main, utilizando el nombre completamente calificado de la clase String:

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

Por último, pondremos 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 que se imprime lo siguiente en la consola de nuestro depurador:

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

Añadamos ahora un punto de interrupción en una línea concreta, aquella en la que la variable app.instanceString se está imprimiendo:

> stop at OurApplication:7

Notamos que se utiliza at después de stop en lugar de in cuando el breakpoint se define en una línea específica.

4.2. Navegar y evaluar

Ahora que hemos establecido nuestros puntos de ruptura, vamos a utilizar cont para continuar la ejecución de nuestro hilo hasta llegar al punto de ruptura de 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 trozo de código:

System.out.println(app.instanceString);

La detención en esta línea también podría haberse hecho deteniéndose en el método main y escribiendo step dos veces. step ejecuta la línea de código actual y detiene el depurador directamente en la siguiente línea.

Ahora que nos hemos detenido, el depurador está evaluando nuestro staticString, el instanceString de la app, la variable local i y finalmente echando un vistazo a cómo evaluar otras expresiones.

Imprimamos staticField en la consola:

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

Ponemos explícitamente el nombre de la clase antes del campo estático.

Imprimamos ahora el campo instancia de app:

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

A continuación, veamos la variable i:

> print ii = 68741

A diferencia de las otras variables, las variables locales no requieren especificar una clase o una instancia. También podemos ver que print tiene exactamente el mismo comportamiento que eval: ambas evalúan una expresión o una variable.

Evaluaremos una nueva instancia de NuestraAplicación a la que hemos pasado un entero como parámetro del constructor:

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

Ahora que hemos evaluado todas las variables que necesitábamos, querremos borrar los puntos de interrupción establecidos anteriormente y dejar que el hilo continúe su procesamiento. Para ello, utilizaremos el comando clear seguido del identificador del breakpoint.

El identificador es exactamente el mismo que el utilizado anteriormente con el comando stop:

> clear OurApplication:7Removed: breakpoint OurApplication:7

Para verificar si el breakpoint ha sido eliminado correctamente, utilizaremos clear sin argumentos. Esto mostrará la lista de breakpoints existentes sin el que acabamos de eliminar:

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

Conclusión

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