Áttekintés

Egy távoli Java alkalmazás hibakeresése több esetben is hasznos lehet.

Ebben a bemutatóban felfedezzük, hogyan lehet ezt megtenni a JDK eszközeinek segítségével.

Az alkalmazás

Kezdjük egy alkalmazás megírásával. Ezt egy távoli helyen futtatjuk, és ezen a cikken keresztül helyben debuggoljuk:

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: A Java Debug Wire Protocol

A Java Debug Wire Protocol egy Java-ban használt protokoll a debugger és a debugger közötti kommunikációra. A debuggee a hibakereső alkalmazás, míg a debugger a hibakereső alkalmazáshoz csatlakozó alkalmazás vagy folyamat.

A két alkalmazás vagy ugyanazon a gépen, vagy különböző gépeken fut. Mi az utóbbira fogunk koncentrálni.

3.1. A JDWP opciói

A JDWP-t a JVM parancssori argumentumaiban fogjuk használni a debuggee alkalmazás indításakor.

A meghívásához egy sor opcióra van szükség:

  • a transport az egyetlen teljesen kötelező opció. Meghatározza, hogy milyen szállítási mechanizmust használjon. dt_shmem csak Windowson működik, és ha mindkét folyamat ugyanazon a gépen fut, míg a dt_socket minden platformmal kompatibilis, és lehetővé teszi, hogy a folyamatok különböző gépeken fussanak
  • szerver nem kötelező opció. Ez a flag, ha be van kapcsolva, meghatározza a debuggerhez való kapcsolódás módját. Vagy kiteszi a folyamatot az address opcióban meghatározott címen keresztül. Ellenkező esetben a JDWP egy alapértelmezettet exponál
  • suspend meghatározza, hogy a JVM felfüggeszti-e a működését és várja-e a debugger csatolását vagy sem
  • address a debugger által exponált címet, általában egy portot tartalmazó opció. Ez egy karaktersorozatként lefordított címet is jelenthet (mint a javadebug, ha Windowson a server=y-t használjuk cím megadása nélkül)

3.2. Parancs indítása

Kezdjük a távoli alkalmazás indításával. Megadjuk az összes korábban felsorolt opciót:

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

A Java 5-ig a runjdwp JVM argumentumot a másik opcióval, a debuggal együtt kellett használni:

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

A JDWP használatának ezt a módját még támogatjuk, de a későbbi kiadásokban elhagyjuk. Lehetőség szerint az újabb jelölés használatát fogjuk előnyben részesíteni.

3.3. A Java 9 óta

A Java 9-es verziójának megjelenésével végre megváltozott a JDWP egyik lehetősége. Ez egy egészen apró változás, mivel csak egy opciót érint, de akkor lesz jelentősége, ha egy távoli alkalmazás hibakeresését próbáljuk elvégezni.

Ez a változás hatással van arra, ahogyan a címzés viselkedik a távoli alkalmazások esetében. A régebbi address=8000 jelölés csak a localhostra vonatkozik. A régi viselkedés eléréséhez a cím előtagjaként egy csillagot használunk kettősponttal (pl. address=*:8000).

A dokumentáció szerint ez nem biztonságos, és ajánlott a hibakereső IP-címét megadni, amikor csak lehetséges:

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

JDB: The Java Debugger

A JDB, azaz a Java Debugger egy olyan eszköz, amely a JDK-ban szerepel, és arra lett kitalálva, hogy a parancssorból kényelmes hibakereső klienst nyújtson.

A JDB indításához az attach módot fogjuk használni. Ez a mód a JDB-t egy futó JVM-hez csatolja. Léteznek más futtatási módok is, mint például a listen vagy a run, de ezek leginkább akkor kényelmesek, ha egy helyben futó alkalmazás hibakeresését végezzük:

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

4.1. Töréspontok

Folytassuk azzal, hogy néhány töréspontot helyezünk el az 1. szakaszban bemutatott alkalmazásban.

Egy töréspontot állítunk be a konstruktorban:

> stop in OurApplication.<init>

Egy másikat a statikus main metódusban, a String osztály teljes minősítésű nevével:

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

Végül az utolsót a buildInstanceString példánymetódusban állítjuk be:

> stop in OurApplication.buildInstanceString(int)

Most észre kell vennünk, hogy a kiszolgáló alkalmazás leáll, és a debugger konzoljára a következőket írja ki:

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

Adjunk most egy töréspontot egy adott sorra, arra, ahol a változó app.instanceString változót nyomtatjuk ki:

> stop at OurApplication:7

Megfigyelhetjük, hogy a stop után az at-t használjuk az in helyett, amikor a töréspontot egy adott sorban definiáljuk.

4.2. A töréspontot egy adott sorban határozzuk meg. Navigálás és kiértékelés

Most, hogy megállítottuk a töréspontokat, használjuk a cont, hogy folytassuk a szálunk végrehajtását, amíg el nem érjük a 7. sorban lévő töréspontot.

A konzolban a következőt kell látnunk kiírva:

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

Emlékeztetésképpen a következő kódrészletet tartalmazó soron álltunk meg:

System.out.println(app.instanceString);

A megállás ezen a soron úgy is megtörténhetett volna, hogy a main metóduson megállunk és kétszer beírjuk a step-et. step végrehajtja az aktuális kódsort és közvetlenül a következő soron megállítja a hibakeresőt.

Most, hogy megálltunk, a debugger kiértékeli a staticStringünket, az alkalmazás instanceStringjét, az i helyi változót, és végül megnézi, hogyan értékelhet ki más kifejezéseket.

Nyomtassuk ki a staticFieldet a konzolra:

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

A static field elé kifejezetten az osztály nevét tettük.

Nyomtassuk ki most az app példánymezőjét:

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

Majd nézzük meg az i változót:

> print ii = 68741

A többi változóval ellentétben a helyi változóknál nem szükséges osztály vagy példány megadása. Azt is láthatjuk, hogy a print pontosan ugyanúgy viselkedik, mint az eval: mindkettő kiértékel egy kifejezést vagy egy változót.

Értelmezzük ki a OurApplication egy új példányát, amelynek konstruktorparamétereként egy egész számot adtunk át:

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

Most, miután kiértékeltük az összes szükséges változót, töröljük a korábban beállított töréspontokat, és hagyjuk, hogy a szál folytassa a feldolgozást. Ehhez a clear parancsot fogjuk használni, amelyet a töréspont azonosítója követ.

Az azonosító pontosan ugyanaz, mint amit korábban a stop parancsnál használtunk:

> clear OurApplication:7Removed: breakpoint OurApplication:7

Hogy ellenőrizzük, hogy a töréspont helyesen lett-e eltávolítva, a clear parancsot használjuk argumentumok nélkül. Ez megjeleníti a meglévő töréspontok listáját, az imént törölt nélkül:

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

Következtetés

Ezzel a gyors cikkel felfedeztük, hogyan használhatjuk a JDWP-t a JDB-vel együtt, mindkét JDK eszközzel együtt.