Á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.
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.
Vélemény, hozzászólás?