Vodič za Java instrumentaciju

1. Uvod

U ovom uputstvu razgovarat ćemo o Java Instrumentation API. Pruža mogućnost dodavanja bajt-koda u postojeće prevedene Java klase.

Također ćemo razgovarati o java agentima i o tome kako ih koristimo za izradu našeg koda.

2. Postavljanje

Kroz članak ćemo izraditi aplikaciju pomoću instrumenata.

Naša aplikacija sastojat će se od dva modula:

  1. Aplikacija na bankomatu koja nam omogućuje podizanje novca
  2. I Java agent koji će nam omogućiti da izmjerimo performanse našeg bankomata mjereći vrijeme uloženo u trošenje novca

Java agent će izmijeniti ATM bajt-kôd omogućujući nam da mjerimo vrijeme povlačenja bez potrebe za izmjenom ATM aplikacije.

Naš projekt imat će sljedeću strukturu:

com.baeldung.instrumentation base 1.0.0 pom agent aplikacija 

Prije nego što se previše pozabavimo detaljima instrumentacije, pogledajmo što je java agent.

3. Što je Java agent

Općenito, java agent je samo posebno izrađena jar datoteka. Koristi API-je za instrumentaciju koji JVM pruža za izmjenu postojećeg byte-koda koji se učitava u JVM.

Da bi agent radio, moramo definirati dvije metode:

  • premain - statički će učitati agent pomoću parametra -javaagent pri pokretanju JVM-a
  • agentmain - će dinamički učitati agent u JVM koristeći Java Attach API

Zanimljiv koncept koji treba imati na umu jest da JVM implementacija, poput Oracle, OpenJDK i drugih, može pružiti mehanizam za dinamičko pokretanje agenata, ali to nije uvjet.

Prvo, pogledajmo kako bismo koristili postojeći Java agent.

Nakon toga ćemo pogledati kako ga možemo stvoriti ispočetka kako bismo dodali funkcionalnost koja nam je potrebna u naš bajt-kod.

4. Učitavanje Java agenta

Da bismo mogli koristiti Java agent, prvo ga moramo učitati.

Imamo dvije vrste tereta:

  • static - koristi se premain za učitavanje agenta pomoću opcije -javaagent
  • dinamičan - koristi agentmain za učitavanje agenta u JVM pomoću Java Attach API-ja

Zatim ćemo pogledati svaku vrstu tereta i objasniti kako to radi.

4.1. Statičko opterećenje

Učitavanje Java agenta pri pokretanju aplikacije naziva se statičkim opterećenjem. Statičko opterećenje mijenja bajt-kôd prilikom pokretanja prije izvršavanja bilo kojeg koda.

Imajte na umu da statičko opterećenje koristi premain metoda, koja će se pokrenuti prije pokretanja bilo kojeg aplikacijskog koda, da bismo ga pokrenuli možemo izvršiti:

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

Važno je napomenuti da uvijek trebamo staviti -javaagent parametar prije -staklenka parametar.

Ispod su zapisnici naše naredbe:

22: 24: 39.296 [main] INFO - [Agent] U premain metodi 22: 24: 39.300 [main] INFO - [Agent] Transforming class MyAtm 22: 24: 39.407 [main] INFO - [Application] Pokretanje aplikacije ATM 22: 24: 41.409 [glavna] INFO - [aplikacija] Uspješno povlačenje [7] jedinica! 22: 24: 41.410 [glavna] INFO - [aplikacija] Operacija povlačenja završena za: 2 sekunde! 22: 24: 53.411 [glavna] INFO - [aplikacija] Uspješno povlačenje [8] jedinica! 22: 24: 53.411 [glavna] INFO - [aplikacija] Operacija povlačenja završena za: 2 sekunde!

Možemo vidjeti kada premain metoda trčala i kada MyAtm razred je transformiran. Također vidimo dva dnevnika transakcija podizanja s bankomata koji sadrže vrijeme potrebno za dovršenje svake operacije.

Imajte na umu da u našoj izvornoj aplikaciji nismo imali vrijeme završetka transakcije, dodao je naš Java agent.

4.2. Dinamičko opterećenje

Postupak učitavanja Java agenta u već pokrenut JVM naziva se dinamičko učitavanje. Agent je priložen pomoću Java Attach API-ja.

Složeniji je scenarij kada već imamo pokrenutu ATM aplikaciju i želimo dinamički zbrajati ukupno vrijeme transakcija bez zastoja u našoj aplikaciji.

Napišimo mali dio koda da bismo to učinili i nazvat ćemo ovu klasu AgentLoader. Radi jednostavnosti stavit ćemo ovu klasu u datoteku jar aplikacije. Dakle, naša datoteka jar datoteke može i pokrenuti našu aplikaciju, i priložiti našeg agenta bankomatnoj aplikaciji:

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

Sad kad imamo svoje AgentLoader, pokrećemo našu aplikaciju osiguravajući da ćemo u deset sekundi pauze između transakcija dinamički priložiti svoj Java agent koristeći AgentLoader.

Dodajmo i ljepilo koje će nam omogućiti ili pokretanje aplikacije ili učitavanje agensa.

Nazvat ćemo ovaj razred Pokretač i to će biti naša glavna klasa jar datoteke:

pokretač javne klase {public static void main (String [] args) baca iznimku {if (args [0] .equals ("StartMyAtmApplication")) {new MyAtmApplication (). run (args); } else if (args [0] .equals ("LoadAgent")) {new AgentLoader (). run (args); }}}

Pokretanje aplikacije

java -jar application.jar StartMyAtmApplication 22: 44: 21.154 [main] INFO - [Application] Pokretanje aplikacije ATM 22: 44: 23.157 [main] INFO - [Application] Uspješno povlačenje [7] jedinica!

Prilaganje Java agenta

Nakon prve operacije, pridružujemo java agent našem JVM-u:

java -jar application.jar LoadAgent 22: 44: 27.022 [glavna] INFO - Pričvršćivanje na ciljni JVM s PID-om: 6575 22: 44: 27.306 [glavna] INFO - Priključeno na ciljni JVM i uspješno učitan Java agent 

Provjerite zapisnike aplikacija

Sad kad smo našeg agenta priključili na JVM, vidjet ćemo da imamo ukupno vrijeme završetka za drugu operaciju povlačenja bankomata.

To znači da smo našu funkcionalnost dodavali u hodu dok je naša aplikacija radila:

22: 44: 27.229 [Priloži slušatelj] INFO - [Agent] U agentmain metodi 22: 44: 27.230 [Priloži slušatelj] INFO - [Agent] Pretvarajući razred MyAtm 22: 44: 33.157 [glavna] INFO - [Primjena] Uspješno povlačenje [8] jedinica! 22: 44: 33.157 [glavna] INFO - [aplikacija] Operacija povlačenja završena za: 2 sekunde!

5. Izrada Java agenta

Nakon što naučimo koristiti agent, pogledajmo kako ga možemo stvoriti. Pogledat ćemo kako koristiti Javassist za promjenu bajt-koda i kombinirat ćemo to s nekim API metodama instrumentacije.

Budući da java agent koristi Java Instrumentation API, prije nego što uđemo preduboko u stvaranje našeg agenta, pogledajmo neke od najčešće korištenih metoda u ovom API-u i kratki opis onoga što rade:

  • addTransformer - dodaje transformator u instrument instrumenta
  • getAllLoadedClasses - vraća niz svih klasa koje trenutno učitava JVM
  • retransformClasses - olakšava instrumentaciju već učitanih klasa dodavanjem bajt-koda
  • removeTransformer - odjavljuje isporučeni transformator
  • redefineClasses - redefinirati isporučeni skup klasa pomoću isporučenih datoteka klasa, što znači da će klasa biti u potpunosti zamijenjena, a ne modificirana kao kod retransformClasses

5.1. Stvorite Premain i Agentmain Metode

Znamo da je svakom Java agentu potreban barem jedan od premain ili agentmain metode. Potonji se koristi za dinamičko učitavanje, dok se prvi koristi za statičko učitavanje java agenta u JVM.

Definirajmo obojicu u našem agentu kako bismo mogli učitati taj agent i statički i dinamički:

javna statička praznina premain (String agentArgs, Instrumentation inst) {LOGGER.info ("[Agent] U premain metodi"); Niz klaseName = "com.baeldung.instrumentation.application.MyAtm"; transformClass (ime klase, inst); } javna statička void agentmain (String agentArgs, Instrumentation inst) {LOGGER.info ("[Agent] In agentmain method"); Niz klaseName = "com.baeldung.instrumentation.application.MyAtm"; transformClass (ime klase, inst); }

U svakoj metodi deklariramo klasu koju želimo promijeniti, a zatim iskopavamo kako bismo transformirali tu klasu pomoću transformClass metoda.

Ispod je kod za transformClass metoda koju smo definirali kako bi nam pomogla u transformaciji MyAtm razred.

U ovoj metodi pronalazimo klasu koju želimo transformirati i koristeći transformirati metoda. Također, transformatoru dodajemo mehanizam za mjerenje:

privatna statička praznina transformClass (Niz klaseName, Instrumentacija instrumentacija) {Class targetCls = null; ClassLoader targetClassLoader = null; // provjerimo možemo li dobiti klasu pomoću forName pokušajte {targetCls = Class.forName (className); targetClassLoader = targetCls.getClassLoader (); transformirati (targetCls, targetClassLoader, instrumentacija); povratak; } catch (Iznimka ex) {LOGGER.error ("Class [{}] not found with Class.forName"); } // u suprotnom ponovite sve učitane klase i pronađite ono što želimo (Class clazz: instrumentation.getAllLoadedClasses ()) {if (clazz.getName (). jednako (ime klase)) {targetCls = clazz; targetClassLoader = targetCls.getClassLoader (); transformirati (targetCls, targetClassLoader, instrumentacija); povratak; }} baciti novi RuntimeException ("Nije uspjelo pronaći klasu [" + className + "]"); } privatna statička void transformacija (Class clazz, ClassLoader classLoader, Instrumentation Instrumentation) {AtmTransformer dt = new AtmTransformer (clazz.getName (), classLoader); instrumentation.addTransformer (dt, true); isprobajte {instrumentation.retransformClasses (clazz); } catch (Exception ex) {throw new RuntimeException ("Transformacija nije uspjela za: [" + clazz.getName () + "]", ex); }}

S ovim na putu, definirajmo transformator za MyAtm razred.

5.2. Definiranje našeg Transformator

Razredni transformator mora implementirati ClassFileTransformer i provesti metodu transformacije.

Koristit ćemo Javassist za dodavanje bajt-koda MyAtm klase i dodajte zapisnik s ukupnim vremenom transakcije povlačenja ATW-a:

javna klasa AtmTransformer implementira ClassFileTransformer {@Override public byte [] transformaciju (učitavač ClassLoader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte [] classfileBuffer) {byte [] byteCode = classfileBuffer; Niz finalTargetClassName = this.targetClassName .replaceAll ("\.", "/"); if (! className.equals (finalTargetClassName)) {return byteCode; } if (className.equals (finalTargetClassName) && loader.equals (targetClassLoader)) {LOGGER.info ("[Agent] Transforming class MyAtm"); isprobajte {ClassPool cp = ClassPool.getDefault (); CtClass cc = cp.get (targetClassName); CtMethod m = cc.getDeclaredMethod (WITHDRAW_MONEY_METHOD); m.addLocalVariable ("vrijeme početka", CtClass.longType); m.insertBefore ("startTime = System.currentTimeMillis ();"); StringBuilder endBlock = novi StringBuilder (); m.addLocalVariable ("vrijeme završetka", CtClass.longType); m.addLocalVariable ("opTime", CtClass.longType); endBlock.append ("endTime = System.currentTimeMillis ();"); endBlock.append ("opTime = (endTime-startTime) / 1000;"); endBlock.append ("LOGGER.info (\" [Aplikacija] Operacija povlačenja završena za: "+" \ "+ opTime + \" sekundi! \ ");"); m.insertAfter (endBlock.toString ()); byteCode = cc.toBytecode (); cc.detach (); } catch (NotFoundException | CannotCompileException | IOException e) {LOGGER.error ("Iznimka", e); }} return byteCode; }}

5.3. Stvaranje datoteke manifesta agenta

Konačno, da bismo dobili djelujući Java agent, trebat će nam manifestna datoteka s nekoliko atributa.

Stoga u službenoj dokumentaciji Instrumentacijskog paketa možemo pronaći cijeli popis manifestnih atributa.

U konačnu datoteku jar agenta Java, u datoteku manifesta dodat ćemo sljedeće redove:

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

Naš agent za instrumentaciju Java sada je gotov. Da biste ga pokrenuli, pogledajte odjeljak Učitavanje Java agenta u ovom članku.

6. Zaključak

U ovom smo članku razgovarali o API-ju Java Instrumentation. Pogledali smo kako učitati Java agent u JVM i statički i dinamički.

Također smo pogledali kako ćemo krenuti u stvaranje vlastitog Java agenta od nule.

Kao i uvijek, cjelovita implementacija primjera može se naći na Githubu.