Overview

Debugar uma Aplicação Java remota pode ser útil em mais de um caso.

Neste tutorial, vamos descobrir como fazer isso usando as ferramentas do JDK.

Aplicação

Comecemos por escrever uma aplicação. Vamos executá-la em um local remoto e depurá-la localmente através deste artigo:

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

O Java Debug Wire Protocol é um protocolo usado em Java para a comunicação entre um debuggee e um debugger. O depurador é a aplicação a ser depurada enquanto o depurador é uma aplicação ou um processo que se liga à aplicação a ser depurada.

Algumas aplicações podem ser executadas na mesma máquina ou em máquinas diferentes. Vamos focar neste último.

3.1. Opções do JDWP

Usaremos JDWP nos argumentos da linha de comando JVM ao iniciar a aplicação de depuração.

A invocação de Its requer uma lista de opções:

  • O transporte é a única opção totalmente necessária. Ele define qual mecanismo de transporte usar. dt_shmem só funciona no Windows e se ambos os processos rodam na mesma máquina enquanto dt_socket é compatível com todas as plataformas e permite que os processos rodem em máquinas diferentes
  • servidor não é uma opção obrigatória. Esta bandeira, quando ativada, define a forma como ela se liga ao depurador. Ele ou expõe o processo através do endereço definido na opção de endereço. Caso contrário, JDWP expõe um padrão
  • suspend define se o JVM deve suspender e esperar que um depurador anexe ou não
  • endereço é a opção que contém o endereço, geralmente uma porta, exposta pelo depurador. Também pode representar um endereço traduzido como uma string de caracteres (como javadebug se usarmos server=y sem fornecer um endereço no Windows)

3.2. Comando de lançamento

Lancemos a aplicação remota. Vamos fornecer todas as opções listadas anteriormente:

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

Até Java 5, o argumento JVM runjdwp teve que ser usado junto com a outra opção debug:

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

Esta forma de usar o JDWP ainda é suportada, mas será descartada em lançamentos futuros. Vamos preferir a utilização da notação mais recente quando possível.

3.3. Desde Java 9

Finalmente, uma das opções do JDWP foi alterada com o lançamento da versão 9 do Java. Esta é uma mudança bem pequena já que diz respeito apenas a uma opção mas fará diferença se estivermos tentando depurar uma aplicação remota.

Esta mudança impacta na maneira como o endereço se comporta para aplicações remotas. O endereço da notação mais antiga=8000 só se aplica ao localhost. Para alcançar o comportamento antigo, usaremos um asterisco com dois pontos como prefixo para o endereço (por exemplo, address=*:8000).

De acordo com a documentação, isto não é seguro e é recomendado especificar o endereço IP do depurador sempre que possível:

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

JDB: O Depurador Java

JDB, o Depurador Java, é uma ferramenta incluída no JDK concebida para fornecer um conveniente cliente depurador a partir da linha de comando.

Para iniciar o JDB, usaremos o modo anexar. Este modo liga o JDB a um JVM em execução. Existem outros modos de execução, como ouvir ou executar, mas são mais convenientes ao depurar uma aplicação em execução local:

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

4.1. Breakpoints

Vamos continuar colocando alguns breakpoints na aplicação apresentada na seção 1.

Ponhamos um breakpoint no construtor:

> stop in OurApplication.<init>

Ponhamos outro no método estático principal, usando o nome totalmente qualificado da classe String:

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

Finalmente, vamos colocar o último no método da instância buildInstanceString:

> stop in OurApplication.buildInstanceString(int)

Agora devemos notar que a aplicação do servidor pára e o seguinte é impresso na nossa consola de depuração:

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

Vamos agora adicionar um breakpoint numa linha específica, aquela em que a aplicação da variável.instanceString está sendo impressa:

> stop at OurApplication:7

Notemos que em é usado após a parada ao invés de em quando o ponto de parada é definido em uma linha específica.

4.2. Navegar e Avaliar

Agora que definimos os nossos breakpoints, vamos usar cont para continuar a execução do nosso thread até atingirmos o breakpoint na linha 7.

Devíamos ver o seguinte impresso no console:

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

Como lembrete, paramos na linha contendo o seguinte trecho de código:

System.out.println(app.instanceString);

Parar nesta linha também poderia ter sido feito parando no método principal e digitando o passo duas vezes. passo executa a linha de código atual e pára o depurador diretamente na linha seguinte.

Agora que paramos, o debugee está avaliando nosso staticString, a instância do aplicativoString, a variável local i e finalmente dando uma olhada em como avaliar outras expressões.

Vamos imprimir o staticField para o console:

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

Pomos explicitamente o nome da classe antes do campo estático.

Vamos agora imprimir o campo de instância do app:

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

Próximo, vamos ver a variável i:

> print ii = 68741

Desse modo como as outras variáveis, as variáveis locais não precisam especificar uma classe ou instância. Também podemos ver que print tem exatamente o mesmo comportamento do eval: ambos avaliam uma expressão ou uma variável.

Avaliaremos uma nova instância de OurApplication para a qual passamos um inteiro como parâmetro construtor:

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

Agora que avaliamos todas as variáveis que precisávamos, vamos querer apagar os breakpoints definidos anteriormente e deixar a thread continuar seu processamento. Para conseguir isso, vamos usar o comando clear seguido do identificador do breakpoint.

O identificador é exatamente o mesmo que o usado anteriormente com o comando stop:

> clear OurApplication:7Removed: breakpoint OurApplication:7

Para verificar se o breakpoint foi removido corretamente, vamos usar clear sem argumentos. Isto irá exibir a lista de breakpoints existentes sem o que acabamos de excluir:

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

Conclusion

I:n este artigo rápido, descobrimos como usar JDWP junto com JDB, ambas ferramentas JDK.