Útmutató a Java műszerekhez
1. Bemutatkozás
Ebben az oktatóanyagban a Java Instrumentation API-ról fogunk beszélni. Lehetőséget nyújt bájtkód hozzáadására a meglévő fordított Java osztályokhoz.
Beszélünk a java ügynökökről és arról is, hogyan használjuk őket a kódunk instrumentálásához.
2. Beállítás
A cikkben felépítünk egy alkalmazást műszerekkel.
Jelentkezésünk két modulból áll:
- ATM alkalmazás, amely lehetővé teszi számunkra a pénzfelvételt
- És egy Java ügynök, amely lehetővé teszi számunkra, hogy az ATM teljesítményét mérjük a befektetett pénzköltség mérésével
A Java ügynök módosítja az ATM bájtkódot, lehetővé téve számunkra az elvonási idő mérését anélkül, hogy módosítanunk kellene az ATM alkalmazást.
Projektünk a következő felépítésű lesz:
com.baeldung.instrumentation base 1.0.0 pom agent alkalmazás
Mielőtt túlságosan belemennénk a hangszerelés részleteibe, nézzük meg, mi az a java-ügynök.
3. Mi a Java ügynök
Általában a java ügynök csak egy speciálisan kialakított jar fájl. Használja a JVM által biztosított Instrumentation API-t, hogy megváltoztassa a JVM-be töltött meglévő bájtkódot.
Az ügynök működéséhez két módszert kell meghatároznunk:
- premain - statikusan betölti az ügynököt a -javaagent paraméter használatával a JVM indításakor
- ügynök - dinamikusan betölti az ügynököt a JVM-be a Java Attach API segítségével
Érdekes szem előtt tartani, hogy egy JVM implementáció, mint például az Oracle, az OpenJDK és mások, mechanizmust biztosíthat az ügynökök dinamikus elindításához, de ez nem követelmény.
Először nézzük meg, hogyan használnánk egy meglévő Java ügynököt.
Ezt követően megvizsgáljuk, hogyan hozhatunk létre egyet a semmiből, hogy a bájtkódunkban hozzáadjuk a szükséges funkcionalitást.
4. Java ügynök betöltése
A Java ügynök használatához először be kell töltenünk.
Kétféle terhelésünk van:
- statikus - használja a premain az ügynök betöltéséhez a -javaagent kapcsolóval
- dinamikus - a ügynök az ügynök betöltése a JVM-be a Java Attach API segítségével
Ezután megnézzük az egyes terhelési típusokat, és elmagyarázzuk annak működését.
4.1. Statikus terhelés
A Java ügynök betöltését az alkalmazás indításakor statikus terhelésnek nevezzük. A statikus terhelés módosítja a bájtkódot az indításkor, mielőtt bármilyen kód végrehajtásra kerülne.
Ne feledje, hogy a statikus terhelés a premain metódus, amely az alkalmazáskód futtatása előtt fog futni, annak futtatásához futtathatjuk:
java -javaagent: agent.jar -jar alkalmazás.jar
Fontos megjegyezni, hogy mindig a -javaagent paraméter a - előttbefőttes üveg paraméter.
Az alábbiakban láthatjuk a parancs naplóit:
22: 24: 39,296 [main] INFO - [Agent] A premain módszerben 22: 24: 39.300 [main] INFO - [Agent] A MyAtm osztály átalakítása 22: 24: 39.407 [main] INFO - [Application] ATM alkalmazás indítása 22 24: 41.409 [main] INFO - [Alkalmazás] [7] egység sikeres visszavonása! 22: 24: 41.410 [main] INFO - [Application] A kivonási művelet 2 másodperc múlva befejeződött! 22: 24: 53.411 [main] INFO - [Alkalmazás] [8] egység sikeres visszavonása! 22: 24: 53.411 [main] INFO - [Alkalmazás] A kivonási művelet 2 másodperc múlva befejeződött!
Láthatjuk, amikor a premain módszer futott és mikor MyAtm osztály átalakult. Látjuk a két ATM-kifizetési naplót is, amelyek tartalmazzák az egyes műveletek végrehajtásához szükséges időt.
Ne feledje, hogy eredeti alkalmazásunkban nem volt ilyen tranzakciónk teljesítési ideje, ezt Java ügynökünk adta hozzá.
4.2. Dinamikus terhelés
A Java-ügynökök már futó JVM-be történő betöltésének folyamatát dinamikus terhelésnek nevezzük. Az ügynököt a Java Attach API segítségével csatolják.
Bonyolultabb forgatókönyv az, amikor már gyártásban fut az ATM-alkalmazásunk, és dinamikusan hozzá akarjuk adni a tranzakciók teljes idejét az alkalmazásunk leállása nélkül.
Írjunk egy kis kóddarabot erre, és ezt az osztályt hívjuk AgentLoader. Az egyszerűség kedvéért ezt az osztályt beletesszük az alkalmazás jar fájljába. Tehát az alkalmazás jar fájlunk elindíthatja az alkalmazást, és az ügynökünket csatolhatja az ATM alkalmazáshoz:
VirtualMachine jvm = VirtualMachine.attach (jvmPid); jvm.loadAgent (agentFile.getAbsolutePath ()); jvm.detach ();
Most, hogy megvan a miénk AgentLoader, azzal kezdjük az alkalmazást, hogy a tranzakciók közötti tíz másodperces szünetben dinamikusan csatoljuk a Java ügynököt a AgentLoader.
Adjunk hozzá olyan ragasztót is, amely lehetővé teszi az alkalmazás elindítását vagy az ügynök betöltését.
Hívjuk ezt az osztályt Indító és ez lesz a fő jar fájl osztályunk:
public class Launcher {public static void main (String [] args) dobja a Kivételt {if (args [0] .equals ("StartMyAtmApplication")) {new MyAtmApplication (). run (args); } else if (args [0] .equals ("LoadAgent")) {új AgentLoader (). run (args); }}}
Az alkalmazás elindítása
java -jar application.jar StartMyAtmApplication 22: 44: 21.154 [main] INFO - [Application] ATM alkalmazás indítása 22: 44: 23.157 [main] INFO - [Application] [7] egység sikeres visszavonása!
Java Agent csatolása
Az első művelet után csatoljuk a java ügynököt a JVM-hez:
java -jar application.jar LoadAgent 22: 44: 27.022 [main] INFO - Csatolás a cél JVM-hez PID-vel: 6575 22: 44: 27.306 [main] INFO - Csatolva a JVM célhoz és sikeresen betöltve a Java ügynököt
Ellenőrizze az alkalmazásnaplókat
Most, hogy ügynökünket a JVM-hez csatlakoztattuk, látni fogjuk, hogy megvan a teljes befejezési idő a második ATM-kivonási művelethez.
Ez azt jelenti, hogy menet közben hozzáadtuk a funkcionalitásunkat, miközben alkalmazásunk futott:
22: 44: 27.229 [Figyelő csatolása] INFO - [Ügynök] Az agentmain módszer 22: 44: 27.230 [Csatolás figyelő] INFO - [Ügynök] MyAtm osztály átalakítása 22: 44: 33.157 [main] INFO - [Alkalmazás] [8] egység! 22: 44: 33.157 [main] INFO - [Alkalmazás] A kivonási művelet 2 másodperc alatt elkészült!
5. Java ügynök létrehozása
Miután megtanulta az ügynök használatát, nézzük meg, hogyan tudunk ilyet létrehozni. Megvizsgáljuk, hogyan lehet a Javassistot használni a bájtkód megváltoztatásához, és ezt egyesítjük néhány instrumentációs API módszerrel.
Mivel egy java ügynök a Java Instrumentation API-t használja, mielőtt túlságosan elmélyülne az ügynökünk létrehozásában, nézzük meg az API leggyakrabban használt módszereit, és rövid leírást a tevékenységükről:
- addTransformer - transzformátort ad a műszermotorhoz
- getAllLoadedClasses - a JVM által jelenleg betöltött összes osztály tömbjét adja vissza
- retransformClasses - bájtkód hozzáadásával megkönnyíti a már betöltött osztályok műszerezését
- removeTransformer - törli a mellékelt transzformátor regisztrációját
- redefineClasses - definiálja újra a mellékelt osztálykészletet a mellékelt osztályfájlok segítségével, ami azt jelenti, hogy az osztály teljesen lecserélésre kerül, és nem úgy módosul, mint a retransformClasses
5.1. Hozza létre a Premain és Agentmain Mód
Tudjuk, hogy minden Java ügynöknek szüksége van legalább az egyikre premain vagy ügynök mód. Az utóbbit dinamikus terhelésre használják, míg az előbbit egy java ügynök statikus betöltésére egy JVM-be.
Definiáljuk mindkettőt az ügynökünkben, hogy statikusan és dinamikusan is betölteni tudjuk ezt az ügynököt:
public static void premain (String agentArgs, Instrumentation inst) {LOGGER.info ("[Ügynök] premain módszerben"); String className = "com.baeldung.instrumentation.application.MyAtm"; transformClass (className, inst); } public static void agentmain (String agentArgs, Instrumentation inst) {LOGGER.info ("[Agent] In agentmain módszer"); String className = "com.baeldung.instrumentation.application.MyAtm"; transformClass (className, inst); }
Mindegyik módszerben kijelentjük azt az osztályt, amelyet meg akarunk változtatni, majd ássuk le, hogy átalakítsuk az osztályt a transformClass módszer.
Az alábbiakban található a transformClass módszer, amelyet az átalakuláshoz definiáltunk MyAtm osztály.
Ebben a módszerben megtaláljuk azt az osztályt, amelyet átalakítani szeretnénk, és felhasználjuk átalakul módszer. Ezenkívül hozzáadjuk a transzformátort a műszer motorjához:
private static void transformClass (String className, Instrumentation instrumentation) {Osztály targetCls = null; ClassLoader targetClassLoader = null; // nézd meg, hogy megkapjuk-e az osztályt a forName használatával, próbáld ki a {targetCls = Osztály.forName (osztályNév) parancsot; targetClassLoader = targetCls.getClassLoader (); transzformáció (targetCls, targetClassLoader, műszer); Visszatérés; } catch (Exception ex) {LOGGER.error ("Osztály [{}] nem található a Class.forName-nél"); } // egyébként ismételje meg az összes betöltött osztályt, és keresse meg, amire vágyunk (Class clazz: instrumentation.getAllLoadedClasses ()) {if (clazz.getName (). egyenlő (className)) {targetCls = clazz; targetClassLoader = targetCls.getClassLoader (); transzformáció (targetCls, targetClassLoader, műszer); Visszatérés; }} dob új RuntimeException ("Nem sikerült megtalálni a (z) [" + osztályNév + "] osztályt"); } privát statikus void transzformáció (Class clazz, ClassLoader classLoader, Instrumentation instrumentation) {AtmTransformer dt = új AtmTransformer (clazz.getName (), classLoader); instrumentation.addTransformer (dt, igaz); próbáld ki a {instrumentation.retransformClasses (clazz); } catch (exeption ex) {dobjon új RuntimeException-t ("Az átalakítás nem sikerült: [" + clazz.getName () + "]", ex); }}
Ezzel eltekintve határozzuk meg a transzformátort MyAtm osztály.
5.2. Sajátunk meghatározása Transzformátor
Osztályú transzformátornak kell megvalósulnia ClassFileTransformer és hajtsa végre az átalakítási módszert.
A Javassist segítségével bájtkódot adunk hozzá MyAtm osztály és adjon hozzá egy naplót az ATW teljes kivonási tranzakció idejével:
public class AtmTransformer megvalósítja a ClassFileTransformer {@Orride public byte [] transzformációt (ClassLoader betöltő, String osztálynév, Class classBeingRedefined, ProtectionDomain protectionDomain, byte [] classfileBuffer) {byte [] byteCode = classfileBuffer; String finalTargetClassName = this.targetClassName .replaceAll ("\.", "/"); if (! className.equals (finalTargetClassName)) {return byteCode; } if (className.equals (finalTargetClassName) && loader.equals (targetClassLoader)) {LOGGER.info ("[Ügynök] MyAtm osztály átalakítása"); próbáld ki a {ClassPool cp = ClassPool.getDefault (); CtClass cc = cp.get (targetClassName); CtMethod m = cc.getDeclaredMethod (WITHDRAW_MONEY_METHOD); m.addLocalVariable ("startTime", CtClass.longType); m.insertBefore ("startTime = System.currentTimeMillis ();"); StringBuilder endBlock = új StringBuilder (); m.addLocalVariable ("endTime", CtClass.longType); m.addLocalVariable ("opTime", CtClass.longType); endBlock.append ("endTime = System.currentTimeMillis ();"); endBlock.append ("opTime = (endTime-startTime) / 1000;"); endBlock.append ("LOGGER.info (\" [Alkalmazás] A visszavonási művelet befejeződött: "+" \ "+ opTime + \" másodperc alatt! \ ");"); m.insertAfter (endBlock.toString ()); byteCode = cc.toBytecode (); cc.detach (); } catch (NotFoundException | CannotCompileException | IOException e) {LOGGER.error ("Kivétel", e); }} return byteCode; }}
5.3. Ügynök manifeszt fájl létrehozása
Végül egy működő Java ügynök megszerzéséhez szükségünk lesz egy pár attribútumot tartalmazó manifeszt fájlra.
Ennélfogva megtalálhatjuk a manifeszt attribútumok teljes listáját az Instrumentation Package hivatalos dokumentációjában.
A végső Java agent jar fájlban a következő sorokat adjuk hozzá a jegyzékfájlhoz:
Agent-Class: com.baeldung.instrumentation.agent.MyInstrumentationAgent Can-Redefine-Classes: true Can-Retransform-Classes: true Premain-class: com.baeldung.instrumentation.agent.MyInstrumentationAgent
Java instrumentációs ügynökünk elkészült. A futtatásához olvassa el a Java ügynök betöltése szakaszát.
6. Következtetés
Ebben a cikkben a Java Instrumentation API-ról beszéltünk. Megvizsgáltuk, hogyan lehet Java ügynököt statikusan és dinamikusan betölteni a JVM-be.
Megvizsgáltuk azt is, hogy miként fogunk a nulláról létrehozni saját Java ügynököt.
Mint mindig, a példa teljes megvalósítása megtalálható a Githubon.