Depuração Remota de Aplicativo Java
1. Visão geral
A depuração de um aplicativo Java remoto pode ser útil em mais de um caso.
Neste tutorial, descobriremos como fazer isso usando as ferramentas do JDK.
2. A aplicação
Vamos começar escrevendo um aplicativo. Vamos executá-lo em um local remoto e depurá-lo localmente por meio 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 !";
}
}
3. JDWP: o protocolo Java Debug Wire
OJava Debug Wire Protocolis a protocol used in Java for the communication between a debuggee and a debugger. O depurador é o aplicativo que está sendo depurado enquanto o depurador é um aplicativo ou um processo que se conecta ao aplicativo que está sendo depurado.
Ambos os aplicativos são executados na mesma máquina ou em máquinas diferentes. Vamos nos concentrar no último.
3.1. Opções do JDWP
Usaremos JDWP nos argumentos de linha de comando da JVM ao iniciar o aplicativo depurado.
Sua invocação requer uma lista de opções:
-
transport é a única opção totalmente necessária. Ele define qual mecanismo de transporte usar. dt_shmem only works on Windows and if both processes run on the same machine enquantodt_socket is compatible with all platforms and allows the processes to run on different machines
-
server não é uma opção obrigatória. Este sinalizador, quando ativado, define a maneira como ele se conecta ao depurador. Ele também expõe o processo por meio do endereço definido na opçãoaddress. Caso contrário, o JDWP expõe um padrão
-
suspend define se a JVM deve suspender e esperar que um depurador se conecte ou não
-
address é a opção que contém o endereço, geralmente uma porta, exposta pelo depurado. Também pode representar um endereço traduzido como uma sequência de caracteres (comojavadebug se usarmosserver=y sem fornecer umaddress no Windows)
3.2. Comando de lançamento
Vamos começar iniciando o aplicativo remoto. Forneceremos todas as opções listadas anteriormente:
java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8000 OurApplication
Até o Java 5, o argumento JVMrunjdwp tinha que ser usado junto com a outra opçãodebug:
java -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=8000
Essa maneira de usar o JDWP ainda é suportada, mas será descartada em versões futuras. Preferiremos o uso da notação mais recente, quando possível.
3.3. Desde Java 9
Finalmente, uma das opções do JDWP mudou com o lançamento da versão 9 do Java. Esta é uma pequena alteração, pois diz respeito apenas a uma opção, mas fará diferença se estivermos tentando depurar um aplicativo remoto.
Essa mudança afeta a maneira comoaddress se comporta para aplicativos remotos. A notação mais antigaaddress=8000 se aplica apenas alocalhost. Para obter o comportamento antigo, usaremos um asterisco com dois pontos como prefixo do endereço (por exemplo,address=*:8000).
De acordo com a documentação, isso 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
4. JDB: o depurador Java
JDB, o Java Debugger, é uma ferramenta incluída no JDK concebida para fornecer um cliente depurador conveniente a partir da linha de comando.
Para iniciar o JDB, usaremos o modoattach. Este modo conecta o JDB a uma JVM em execução. Existem outros modos de execução, comolisten ourun, mas são mais convenientes ao depurar um aplicativo em execução local:
jdb -attach 127.0.0.1:8000
> Initializing jdb ...
4.1. Pontos de interrupção
Vamos continuar colocando alguns pontos de interrupção no aplicativo apresentado na seção 1.
Definiremos um ponto de interrupção no construtor:
> stop in OurApplication.
Definiremos outro no método estáticomain, usando o nome totalmente qualificado da classeString:
> stop in OurApplication.main(java.lang.String[])
Finalmente, definiremos o último no método de instânciabuildInstanceString:
> stop in OurApplication.buildInstanceString(int)
Agora devemos observar a parada do aplicativo do servidor e o seguinte sendo impresso em nosso console do depurador:
> Breakpoint hit: "thread=main", OurApplication.(), line=11 bci=0
Vamos agora adicionar um ponto de interrupção em uma linha específica, aquela onde a variávelapp.instanceString está sendo impressa:
> stop at OurApplication:7
Notamos queat é usado apósstop em vez dein quando o ponto de interrupção é definido em uma linha específica.
4.2. Navegar e avaliar
Agora que definimos nossos pontos de interrupção, vamos usarcont para continuar a execução de nosso segmento até chegarmos ao ponto de interrupção na linha 7.
Deveríamos ver o seguinte impresso no console:
> Breakpoint hit: "thread=main", OurApplication.main(), line=7 bci=17
Como um lembrete, paramos na linha que contém o seguinte trecho de código:
System.out.println(app.instanceString);
Parar nesta linha também poderia ter sido feito parando no métodomaine digitandostep duas vezes. step executa a linha de código atual e para o depurador diretamente na próxima linha.
Agora que paramos, o depurado está avaliando nossostaticString, oappinstanceString, a variável localie finalmente dando uma olhada em como avaliar outros expressões.
Vamos imprimirstaticField no console:
> eval OurApplication.staticString
OurApplication.staticString = "Static String"
Colocamos explicitamente o nome da classe antes do campo estático.
Vamos agora imprimir o campo de instância deapp:
> eval app.instanceString
app.instanceString = "68741. Instance String !"
A seguir, vamos ver a variáveli:
> print i
i = 68741
Ao contrário das outras variáveis, as variáveis locais não exigem a especificação de uma classe ou instância. Também podemos ver queprint tem exatamente o mesmo comportamento queeval: ambos avaliam uma expressão ou uma variável.
Avaliaremos uma nova instância deOurApplication para a qual passamos um número inteiro como um parâmetro do construtor:
> print new OurApplication(10).instanceString
new OurApplication(10).instanceString = "10. Instance String !"
Agora que avaliamos todas as variáveis de que precisávamos, queremos excluir os pontos de interrupção definidos anteriormente e deixar o thread continuar seu processamento. Para conseguir isso, vamos usar o comandoclear seguido pelo identificador do ponto de interrupção.
O identificador é exatamente igual ao usado anteriormente com o comandostop:
> clear OurApplication:7
Removed: breakpoint OurApplication:7
Para verificar se o ponto de interrupção foi removido corretamente, usaremosclear sem argumentos. Isso exibirá a lista de pontos de interrupção existentes sem o que acabamos de excluir:
> clear
Breakpoints set:
breakpoint OurApplication.
breakpoint OurApplication.buildInstanceString(int)
breakpoint OurApplication.main(java.lang.String[])