Sprievodca inštrumentáciou Java

1. Úvod

V tomto výučbe si povieme niečo o Java Instrumentation API. Poskytuje možnosť pridať bajtový kód k existujúcim zostaveným triedam Java.

Tiež si povieme niečo o agentoch Java a o tom, ako ich používame na vybavenie nášho kódu.

2. Inštalácia

V celom článku vytvoríme aplikáciu pomocou prístrojového vybavenia.

Naša aplikácia bude pozostávať z dvoch modulov:

  1. Aplikácia ATM, ktorá nám umožňuje vyberať peniaze
  2. A agent Java, ktorý nám umožní merať výkonnosť nášho bankomatu meraním času investovaného investovaním peňazí

Agent Java upraví bajtový kód bankomatu, čo nám umožní merať čas výberu bez toho, aby sme museli upravovať aplikáciu bankomatu.

Náš projekt bude mať nasledujúcu štruktúru:

com.baeldung.instrumentation base 1.0.0 aplikácia agenta pom 

Predtým, ako sa začneme venovať podrobnostiam prístrojovej techniky, pozrime sa, čo je agent Java.

3. Čo je to agent Java

Agent Java je vo všeobecnosti iba špeciálne vytvorený súbor jar. Využíva inštrumentačné API, ktoré poskytuje JVM na zmenu existujúceho bajtového kódu, ktorý je načítaný v JVM.

Aby agent fungoval, musíme definovať dve metódy:

  • premain - pri spustení JVM staticky načíta agenta pomocou parametra -javaagent
  • hlavný agent - dynamicky načíta agenta do JVM pomocou rozhrania Java Attach API

Je potrebné mať na pamäti zaujímavý koncept, že implementácia JVM, ako napríklad Oracle, OpenJDK a ďalšie, môže poskytnúť mechanizmus na dynamické spustenie agentov, ale nie je to požiadavka.

Najprv sa pozrime, ako by sme použili existujúceho agenta Java.

Potom sa pozrieme na to, ako môžeme vytvoriť jeden od nuly a pridať tak funkčnosť, ktorú potrebujeme, do nášho bajtového kódu.

4. Načítanie agenta Java

Aby sme mohli používať agenta Java, musíme ho najskôr načítať.

Máme dva typy zaťaženia:

  • statický - využíva premain načítať agenta pomocou voľby -javaagent
  • dynamický - využíva hlavný agent načítať agenta do JVM pomocou rozhrania Java Attach API

Ďalej sa pozrieme na jednotlivé typy záťaže a vysvetlíme, ako to funguje.

4.1. Statické zaťaženie

Načítanie agenta Java pri štarte aplikácie sa nazýva statické načítanie. Statické zaťaženie upravuje bajtový kód v čase spustenia pred vykonaním ľubovoľného kódu.

Majte na pamäti, že statické zaťaženie využíva premain metóda, ktorá bude bežať pred spustením ľubovoľného kódu aplikácie, aby sme ju mohli spustiť, môžeme vykonať:

java -javaagent: agent.jar -jar application.jar

Je dôležité poznamenať, že by sme mali vždy dávať -javaagent parameter pred -jar parameter.

Ďalej sú uvedené protokoly nášho príkazu:

22: 24: 39.296 [hlavná] INFO - [Agent] V prvotnej metóde 22: 24: 39.300 [hlavná] INFO - [Agent] Transformujúca trieda MyAtm 22: 24: 39.407 [hlavná] INFO - [Aplikácia] Spustenie aplikácie ATM 22: 24: 41.409 [hlavné] INFO - [Aplikácia] Úspešný výber [7] jednotiek! 22: 24: 41.410 [hlavné] INFO - [prihláška] Operácia výberu bola dokončená za: 2 sekundy! 22: 24: 53,411 [hlavné] INFO - [Aplikácia] Úspešný výber [8] jednotiek! 22: 24: 53,411 [hlavné] INFO - [prihláška] Operácia výberu bola dokončená za: 2 sekundy!

Uvidíme, kedy premain metóda prebehla a kedy MyAtm triedy bola transformovaná. Vidíme tiež dva protokoly transakcií výberu z bankomatu, ktoré obsahujú čas potrebný na dokončenie každej operácie.

Pamätajte, že v našej pôvodnej aplikácii sme tento čas dokončenia transakcie nemali, pridal ho náš agent Java.

4.2. Dynamické zaťaženie

Postup načítania agenta Java do už spusteného JVM sa nazýva dynamické načítanie. Agent je pripojený pomocou rozhrania Java Attach API.

Zložitejším scenárom je, keď už máme našu ATM aplikáciu spustenú vo výrobe a chceme pridať celkový čas transakcií dynamicky bez výpadkov našej aplikácie.

Napíšme malý kúsok kódu, aby sme to dosiahli, a nazveme túto triedu AgentLoader. Pre zjednodušenie vložíme túto triedu do súboru jar aplikácie. Takže náš súbor jar aplikácie môže spustiť našu aplikáciu aj pripojiť nášho agenta k aplikácii ATM:

VirtualMachine jvm = VirtualMachine.attach (jvmPid); jvm.loadAgent (agentFile.getAbsolutePath ()); jvm.detach ();

Teraz, keď máme svoje AgentLoader, spustíme našu aplikáciu a uistíme sa, že v desaťsekundovej pauze medzi transakciami dynamicky pripojíme nášho agenta Java pomocou znaku AgentLoader.

Pridajme tiež lepidlo, ktoré nám umožní buď spustiť aplikáciu, alebo načítať agenta.

Zavoláme túto triedu Launcher a bude to naša hlavná trieda súborov jar:

public class Launcher {public static void main (String [] args) vyvolá Výnimku {if (args [0] .equals ("StartMyAtmApplication")) {new MyAtmApplication (). run (args); } else if (args [0] .equals ("LoadAgent")) {new AgentLoader (). run (args); }}}

Spustenie aplikácie

java -jar application.jar StartMyAtmApplication 22: 44: 21.154 [main] INFO - [Application] Spustenie aplikácie ATM 22: 44: 23.157 [main] INFO - [Application] Úspešný výber [7] jednotiek!

Pripojenie agenta Java

Po prvej operácii pripojíme java agenta k nášmu JVM:

java -jar application.jar LoadAgent 22: 44: 27.022 [hlavné] INFO - pripojenie k cieľovému JVM s PID: 6575 22: 44: 27.306 [hlavné] INFO - pripojené k cieľovému JVM a úspešne načítaný agent Java 

Skontrolujte denníky aplikácií

Teraz, keď sme pripojili nášho agenta k JVM, uvidíme, že máme celkový čas na dokončenie druhej operácie výberu z bankomatu.

To znamená, že sme pridali našu funkčnosť za behu, keď bola naša aplikácia spustená:

22: 44: 27.229 [Pripojiť poslucháča] INFO - [Agent] V metóde agentmain 22: 44: 27.230 [Pripojiť poslucháča] INFO - [Agent] Transformujúca sa trieda MyAtm 22: 44: 33,157 [hlavné] INFO - [Aplikácia] Úspešné stiahnutie [8] jednotiek! 22: 44: 33.157 [hlavné] INFO - [prihláška] Operácia výberu bola dokončená za: 2 sekundy!

5. Vytvorenie agenta Java

Keď sa naučíme používať agenta, pozrime sa, ako ho môžeme vytvoriť. Pozrime sa na to, ako použiť Javassist na zmenu bajtového kódu, a skombinujeme to s niektorými metódami prístrojového rozhrania API.

Pretože agent Java používa rozhranie Java Instrumentation API, predtým, ako sa pustíme do vytvárania nášho agenta, pozrime sa na niektoré z najpoužívanejších metód v tomto API a krátky popis toho, čo robia:

  • addTransformer - pridáva transformátor do prístrojového vybavenia
  • getAllLoadedClasses - vráti pole všetkých tried, ktoré sú momentálne načítané JVM
  • retransformačné triedy - uľahčuje prístrojové vybavenie už načítaných tried pridaním bajtového kódu
  • removeTransformer - zruší registráciu dodávaného transformátora
  • predefinovaťTriedy - predefinovať dodanú sadu tried pomocou dodaných súborov tried, čo znamená, že trieda bude úplne nahradená a nebude sa upravovať ako v prípade retransformačné triedy

5.1. Vytvorte Premain a Hlavný agent Metódy

Vieme, že každý agent Java potrebuje aspoň jeden z týchto súborov premain alebo hlavný agent metódy. Posledná zmienka sa používa na dynamické načítanie, zatiaľ čo druhá sa používa na statické načítanie agenta Java do JVM.

Definujme ich oboch v našom agentovi, aby sme mohli tohto agenta načítať staticky aj dynamicky:

public static void premain (String agentArgs, Instrumentation inst) {LOGGER.info ("[Agent] In premain method"); Reťazec className = "com.baeldung.instrumentation.application.MyAtm"; transformClass (className, inst); } public static void agentmain (String agentArgs, Instrumentation inst) {LOGGER.info ("[Agent] V agentmain metóde"); Reťazec className = "com.baeldung.instrumentation.application.MyAtm"; transformClass (className, inst); }

V každej metóde deklarujeme triedu, ktorú chceme zmeniť, a potom prehĺbime transformáciu tejto triedy pomocou znaku transformClass metóda.

Nižšie je uvedený kód pre transformClass metóda, ktorú sme definovali, aby nám pomohla transformovať sa MyAtm trieda.

V tejto metóde nájdeme triedu, ktorú chceme transformovať, a pomocou transformovať metóda. Tiež pridáme transformátor do prístrojového vybavenia:

private static void transformClass (String className, Instrumentation instrumentation) {Class targetCls = null; ClassLoader targetClassLoader = null; // uvidíme, či môžeme triedu získať pomocou forName try {targetCls = Class.forName (className); targetClassLoader = targetCls.getClassLoader (); transformácia (targetCls, targetClassLoader, prístrojové vybavenie); návrat; } catch (Exception ex) {LOGGER.error ("Class [{}] not found with Class.forName"); } // inak iterujeme všetky načítané triedy a nájdeme, čo chceme (Class clazz: instrumentation.getAllLoadedClasses ()) {if (clazz.getName (). equals (className)) {targetCls = clazz; targetClassLoader = targetCls.getClassLoader (); transformácia (targetCls, targetClassLoader, prístrojové vybavenie); návrat; }} hodiť novú RuntimeException ("Nepodarilo sa nájsť triedu [" + className + "]"); } transformácia súkromnej statickej prázdnoty (Class clazz, ClassLoader classLoader, prístrojové vybavenie) {AtmTransformer dt = nový AtmTransformer (clazz.getName (), classLoader); instrumentation.addTransformer (dt, true); try {instrumentation.retransformClasses (clazz); } catch (Exception ex) {throw new RuntimeException ("Transformation failed for: [" + clazz.getName () + "]", ex); }}

Ak to nie je v ceste, definujme transformátor pre MyAtm trieda.

5.2. Definovanie našej Transformátor

Musí byť implementovaný transformátor triedy ClassFileTransformer a implementovať metódu transformácie.

Na pridanie bajtového kódu použijeme Javassist MyAtm triedy a pridať protokol s celkovým časom transakcie výberu ATW:

public class AtmTransformer implements ClassFileTransformer {@Override public byte [] transform (ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte [] classfileBuffer) {byte [] byteCode = classfileBuffer; Reťazec finalTargetClassName = this.targetClassName .replaceAll ("\.", "/"); if (! className.equals (finalTargetClassName)) {návrat byteCode; } if (className.equals (finalTargetClassName) && loader.equals (targetClassLoader)) {LOGGER.info ("[Agent] Transformujúca trieda MyAtm"); try {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 = nový 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 (\" [Aplikácia] Operácia výberu ukončená za: "+" \ "+ opTime + \" sekundy! \ ");"); m.insertAfter (endBlock.toString ()); byteCode = cc.toBytecode (); cc.detach (); } catch (NotFoundException | CannotCompileException | IOException e) {LOGGER.error ("Exception", e); }} vratit byteCode; }}

5.3. Vytvorenie súboru manifestu agenta

Nakoniec, aby sme získali fungujúceho agenta Java, budeme potrebovať súbor manifestu s niekoľkými atribútmi.

Celý zoznam atribútov manifestu preto nájdeme v oficiálnej dokumentácii balíka nástrojov.

V konečnom súbore jar agenta Java pridáme do súboru manifestu nasledujúce riadky:

Trieda agenta: com.baeldung.instrumentation.agent.MyInstrumentationAgent Can-Redefine-Classes: true Can-Retransform-Classes: true Premain-Class: com.baeldung.instrumentation.agent.MyInstrumentationAgent

Náš agent inštrumentácie Java je teraz hotový. Jeho spustenie nájdete v časti Načítanie agenta Java tohto článku.

6. Záver

V tomto článku sme hovorili o rozhraní Java Instrumentation API. Pozreli sme sa na to, ako načítať agenta Java do JVM staticky aj dynamicky.

Pozreli sme sa tiež na to, ako by sme od nuly postupovali pri vytváraní nášho vlastného agenta Java.

Celú implementáciu príkladu nájdete ako vždy na serveri Github.


$config[zx-auto] not found$config[zx-overlay] not found