Przegląd

Debugowanie zdalnej aplikacji Java może być przydatne w więcej niż jednym przypadku.

W tym tutorialu, odkryjemy jak to zrobić używając narzędzi JDK.

Aplikacja

Zacznijmy od napisania aplikacji. Uruchomimy ją zdalnie i będziemy debugować lokalnie dzięki temu artykułowi:

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

The Java Debug Wire Protocol jest protokołem używanym w Javie do komunikacji między debuggerem a debuggerem. Debuggee jest aplikacją, która jest debugowana, podczas gdy debugger jest aplikacją lub procesem łączącym się z aplikacją, która jest debugowana.

Obydwie aplikacje albo działają na tej samej maszynie, albo na różnych maszynach. Skupimy się na tym drugim przypadku.

3.1. Opcje JDWP

Będziemy używać JDWP w argumentach wiersza poleceń JVM podczas uruchamiania aplikacji debuggee.

Jego wywołanie wymaga listy opcji:

  • transport jest jedyną w pełni wymaganą opcją. dt_shmem działa tylko w Windows i jeśli oba procesy działają na tej samej maszynie, podczas gdy dt_socket jest kompatybilny ze wszystkimi platformami i pozwala procesom działać na różnych maszynach
  • server nie jest obowiązkową opcją. Ta flaga, gdy jest włączona, definiuje sposób dołączania do debuggera. Albo wystawia proces poprzez adres zdefiniowany w opcji adres. W przeciwnym razie, JDWP wystawia domyślny
  • suspend definiuje, czy JVM powinna się zawiesić i czekać na dołączenie debuggera, czy nie
  • address jest opcją zawierającą adres, zwykle port, wystawiony przez debugger. Może też reprezentować adres przetłumaczony jako ciąg znaków (jak javadebug jeśli użyjemy server=y bez podania adresu w Windows)

3.2. Launch Command

Zacznijmy od uruchomienia zdalnej aplikacji. Zapewnimy wszystkie opcje wymienione wcześniej:

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

Do Javy 5, argument JVM runjdwp musiał być używany razem z inną opcją debug:

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

Ten sposób używania JDWP jest nadal wspierany, ale zostanie porzucony w przyszłych wydaniach. Będziemy preferować używanie nowszej notacji, gdy będzie to możliwe.

3.3. Od wersji Java 9

Wreszcie, jedna z opcji JDWP zmieniła się wraz z wydaniem wersji 9 Javy. Jest to dość drobna zmiana, ponieważ dotyczy tylko jednej opcji, ale będzie miała znaczenie, jeśli będziemy próbowali debugować zdalną aplikację.

Zmiana ta wpływa na sposób, w jaki zachowuje się adres dla zdalnych aplikacji. Starsza notacja address=8000 odnosi się tylko do localhost. Aby osiągnąć stare zachowanie, użyjemy gwiazdki z dwukropkiem jako przedrostka adresu (np. address=*:8000).

Zgodnie z dokumentacją, nie jest to bezpieczne i zaleca się podawanie adresu IP debuggera, gdy tylko jest to możliwe:

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

JDB: The Java Debugger

JDB, Java Debugger, jest narzędziem zawartym w JDK stworzonym w celu zapewnienia wygodnego klienta debuggera z linii poleceń.

Aby uruchomić JDB, użyjemy trybu dołączania. Ten tryb dołącza JDB do działającej maszyny JVM. Istnieją inne tryby pracy, takie jak listen lub run, ale są one głównie wygodne podczas debugowania lokalnie działającej aplikacji:

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

4.1. Punkty przerwania

Dalej kontynuujmy, umieszczając kilka punktów przerwania w aplikacji przedstawionej w rozdziale 1.

Punkt przerwania ustawimy na konstruktorze:

> stop in OurApplication.<init>

Kolejny ustawimy w statycznej metodzie main, wykorzystującej w pełni kwalifikowaną nazwę klasy String:

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

Wreszcie ostatni ustawimy na metodzie instancyjnej buildInstanceString:

> stop in OurApplication.buildInstanceString(int)

Powinniśmy teraz zauważyć, że aplikacja serwera zatrzymuje się, a w naszej konsoli debuggera wypisywana jest następująca informacja:

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

Dodajmy teraz punkt przerwania na konkretnej linii, tej, w której wypisywana jest zmienna app.instanceString jest wypisywana:

> stop at OurApplication:7

Zauważmy, że at jest używane po stop zamiast in, gdy punkt przerwania jest zdefiniowany w konkretnej linii.

4.2. Navigate and Evaluate

Teraz, gdy ustawiliśmy nasze punkty przerwania, użyjmy cont, aby kontynuować wykonywanie naszego wątku, dopóki nie osiągniemy punktu przerwania w linii 7.

W konsoli powinniśmy zobaczyć następujący tekst:

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

Przypominamy, że zatrzymaliśmy się na linii zawierającej następujący fragment kodu:

System.out.println(app.instanceString);

Zatrzymanie się na tej linii mogło być również wykonane przez zatrzymanie się na metodzie main i dwukrotne wpisanie polecenia step. step wykonuje bieżącą linię kodu i zatrzymuje debugger bezpośrednio na następnej linii.

Teraz, gdy już się zatrzymaliśmy, debuger ocenia nasz staticString, instanceString aplikacji, zmienną lokalną i i w końcu przygląda się, jak oceniać inne wyrażenia.

Wypisujmy staticField na konsolę:

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

Wyraźnie umieściliśmy nazwę klasy przed polem statycznym.

Wydrukujmy teraz pole instancji aplikacji:

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

Następnie przyjrzyjmy się zmiennej i:

> print ii = 68741

W przeciwieństwie do innych zmiennych, zmienne lokalne nie wymagają określenia klasy lub instancji. Widzimy również, że print ma dokładnie takie samo zachowanie jak eval: oba oceniają wyrażenie lub zmienną.

Obliczymy nową instancję OurApplication, dla której przekazaliśmy liczbę całkowitą jako parametr konstruktora:

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

Teraz, gdy oceniliśmy wszystkie zmienne, które potrzebowaliśmy, będziemy chcieli usunąć ustawione wcześniej punkty przerwania i pozwolić wątkowi kontynuować przetwarzanie. W tym celu użyjemy polecenia clear, po którym podamy identyfikator punktu przerwania.

Ten identyfikator jest dokładnie taki sam, jak ten użyty wcześniej w poleceniu stop:

> clear OurApplication:7Removed: breakpoint OurApplication:7

Aby sprawdzić, czy punkt przerwania został poprawnie usunięty, użyjemy clear bez argumentów. Spowoduje to wyświetlenie listy istniejących punktów przerwania bez tego, który właśnie usunęliśmy:

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

Zakończenie

I:W tym krótkim artykule dowiedzieliśmy się, jak używać JDWP razem z JDB, obydwoma narzędziami JDK.

.