Veiledning til Java-instrumentering

1. Introduksjon

I denne opplæringen skal vi snakke om Java Instrumentation API. Det gir muligheten til å legge til byte-kode til eksisterende kompilerte Java-klasser.

Vi vil også snakke om java-agenter og hvordan vi bruker dem til å instrumentere koden vår.

2. Oppsett

Gjennom artikkelen bygger vi en app ved hjelp av instrumentering.

Søknaden vår vil bestå av to moduler:

  1. En minibankapp som lar oss ta ut penger
  2. Og en Java-agent som vil tillate oss å måle ytelsen til minibanken ved å måle den investerte tiden

Java-agenten vil endre ATM-byte-koden slik at vi kan måle uttakstid uten å måtte endre ATM-appen.

Prosjektet vårt vil ha følgende struktur:

com.baeldung.instrumentation base 1.0.0 applikasjon for pom-agenter 

Før vi går for mye inn i detaljene i instrumentering, la oss se hva en java-agent er.

3. Hva er en Java Agent

Generelt er en java-agent bare en spesiallaget jar-fil. Den bruker Instrumentation API som JVM gir for å endre eksisterende byte-kode som er lastet inn i en JVM.

For at en agent skal fungere, må vi definere to metoder:

  • premain - laster agenten statisk ved hjelp av -javaagent-parameteren ved oppstart av JVM
  • agenthoved - vil laste agenten dynamisk inn i JVM ved hjelp av Java Attach API

Et interessant konsept å huske på er at en JVM-implementering, som Oracle, OpenJDK og andre, kan gi en mekanisme for å starte agenter dynamisk, men det er ikke et krav.

La oss først se hvordan vi bruker en eksisterende Java-agent.

Etter det vil vi se på hvordan vi kan lage en fra bunnen av for å legge til funksjonaliteten vi trenger i byte-koden vår.

4. Laste inn en Java Agent

For å kunne bruke Java-agenten, må vi først laste den inn.

Vi har to typer last:

  • statisk - benytter seg av premain for å laste agenten med alternativet -javaagent
  • dynamisk - benytter seg av agenthoved for å laste agenten inn i JVM ved hjelp av Java Attach API

Deretter tar vi en titt på hver type last og forklarer hvordan den fungerer.

4.1. Statisk belastning

Lasting av en Java-agent ved oppstart av programmet kalles statisk belastning. Statisk belastning endrer byte-koden ved oppstartstid før noen kode utføres.

Husk at den statiske belastningen bruker premain metode, som vil kjøre før en hvilken som helst applikasjonskode kjører, for å få den til å kjøre kan vi utføre:

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

Det er viktig å merke seg at vi alltid bør sette -javaagent parameter før -krukke parameter.

Nedenfor er loggene for kommandoen vår:

22: 24: 39.296 [main] INFO - [Agent] In premain method 22: 24: 39.300 [main] INFO - [Agent] Transforming class MyAtm 22: 24: 39.407 [main] INFO - [Application] Starter minibankapplikasjon 22: 24: 41.409 [main] INFO - [Application] Vellykket tilbaketrekking av [7] enheter! 22: 24: 41.410 [main] INFO - [Application] Tilbaketrekning fullført på: 2 sekunder! 22: 24: 53.411 [main] INFO - [Application] Vellykket tilbaketrekking av [8] enheter! 22: 24: 53.411 [main] INFO - [Application] Tilbaketrekning fullført på: 2 sekunder!

Vi kan se når premain metoden kjørte og når MyAtm klassen ble forvandlet. Vi ser også de to loggene for uttak av minibanker som inneholder tiden det tok hver operasjon å fullføre.

Husk at i vår opprinnelige applikasjon hadde vi ikke denne fullføringstidspunktet for en transaksjon, den ble lagt til av vår Java-agent.

4.2. Dynamisk belastning

Prosedyren for å laste en Java-agent inn i en allerede kjørende JVM kalles dynamisk belastning. Agenten er tilknyttet ved hjelp av Java Attach API.

Et mer komplekst scenario er når vi allerede har minibankapplikasjonen som kjører i produksjon, og vi vil legge til den totale tiden av transaksjoner dynamisk uten nedetid for applikasjonen vår.

La oss skrive et lite stykke kode for å gjøre nettopp det, og vi vil kalle denne klassen AgentLoader. For enkelhets skyld legger vi denne klassen i applikasjons jar-filen. Så vår applikasjons jar-fil kan både starte søknaden vår og knytte agenten til ATM-applikasjonen:

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

Nå som vi har vår AgentLoader, vi starter søknaden vår og sørger for at i ti sekunders pause mellom transaksjonene, legger vi Java-agenten dynamisk ved hjelp av AgentLoader.

La oss også legge til limet som lar oss enten starte applikasjonen eller laste agenten.

Vi vil kalle denne klassen Launcher og det vil være vår viktigste jar-filklasse:

offentlig klasse Launcher {public static void main (String [] args) kaster Unntak {if (args [0] .equals ("StartMyAtmApplication")) {new MyAtmApplication (). run (args); } annet hvis (args [0] .equals ("LoadAgent")) {new AgentLoader (). run (args); }}}

Starter applikasjonen

java -jar application.jar StartMyAtmApplication 22: 44: 21.154 [main] INFO - [Application] Starter minibankapplikasjon 22: 44: 23.157 [main] INFO - [Application] Vellykket tilbaketrekking av [7] enheter!

Feste Java Agent

Etter den første operasjonen, fester vi java-agenten til vår JVM:

java -jar application.jar LoadAgent 22: 44: 27.022 [main] INFO - Vedlegg til mål JVM med PID: 6575 22: 44: 27.306 [main] INFO - Vedlagt til mål JVM og lastet Java-agent vellykket 

Sjekk applikasjonslogger

Nå som vi tilknyttet agenten vår til JVM, ser vi at vi har den totale ferdigstillelsestiden for den andre minibankuttaket.

Dette betyr at vi har lagt til funksjonaliteten vår mens applikasjonen kjører:

22: 44: 27.229 [Fest lytter] INFO - [Agent] I agenthovedmetode 22: 44: 27.230 [Fest lytter] INFO - [Agent] Transformerende klasse MyAtm 22: 44: 33.157 [main] INFO - [Application] Vellykket tilbaketrekking av [8] enheter! 22: 44: 33.157 [main] INFO - [Application] Tilbaketrekning fullført på: 2 sekunder!

5. Opprette en Java Agent

Etter å ha lært hvordan vi bruker en agent, la oss se hvordan vi kan lage en. Vi ser på hvordan du bruker Javassist til å endre byte-kode, og vi kombinerer dette med noen API-metoder for instrumentering.

Siden en java-agent bruker Java Instrumentation API, la oss se noen av de mest brukte metodene i denne APIen, og en kort beskrivelse av hva de gjør, før vi går for dypt inn i å lage vår agent.

  • addTransformer - legger til en transformator til instrumentasjonsmotoren
  • getAllLoadedClasses - returnerer en matrise med alle klasser som for øyeblikket er lastet av JVM
  • retransformClasses - muliggjør instrumentering av allerede lastede klasser ved å legge til byte-kode
  • Fjern Transformer - avregistrerer den medfølgende transformatoren
  • omdefinere briller - omdefinere det medfølgende settet med klasser ved hjelp av de medfølgende klassefilene, noe som betyr at klassen vil bli fullstendig erstattet, ikke endret som med retransformClasses

5.1. Opprett Premain og Agentmain Metoder

Vi vet at hver Java-agent trenger minst en av premain eller agenthoved metoder. Sistnevnte brukes til dynamisk belastning, mens førstnevnte brukes til statisk å laste et Java-agent inn i en JVM.

La oss definere dem begge i agenten vår, slik at vi kan laste denne agenten både statisk og dynamisk:

offentlig statisk ugyldig premain (String agentArgs, Instrumentation inst) {LOGGER.info ("[Agent] In premain method"); String className = "com.baeldung.instrumentation.application.MyAtm"; transformClass (className, inst); } public static void agentmain (String agentArgs, Instrumentation inst) {LOGGER.info ("[Agent] In agentmain method"); String className = "com.baeldung.instrumentation.application.MyAtm"; transformClass (className, inst); }

I hver metode erklærer vi klassen vi vil endre, og graver deretter ned for å transformere den klassen ved hjelp av transformClass metode.

Nedenfor er koden for transformClass metoden som vi definerte for å hjelpe oss med å transformere MyAtm klasse.

I denne metoden finner vi klassen vi vil transformere og bruker forvandle metode. Vi legger også transformatoren til instrumentasjonsmotoren:

private static void transformClass (String className, Instrumentation instrumentation) {Class targetCls = null; ClassLoader targetClassLoader = null; // se om vi kan få klassen ved hjelp av forName, prøv {targetCls = Class.forName (className); targetClassLoader = targetCls.getClassLoader (); transform (targetCls, targetClassLoader, instrumentering); komme tilbake; } fange (Unntak ex) {LOGGER.error ("Klasse [{}] ikke funnet med Class.forName"); } // ellers gjentar du alle lastede klasser og finner det vi ønsker (Class clazz: instrumentation.getAllLoadedClasses ()) {if (clazz.getName (). tilsvarer (className)) {targetCls = clazz; targetClassLoader = targetCls.getClassLoader (); transform (targetCls, targetClassLoader, instrumentering); komme tilbake; }} kast ny RuntimeException ("Kunne ikke finne klasse [" + className + "]"); } privat statisk tomromstransformasjon (Class clazz, ClassLoader classLoader, Instrumentation instrumentation) {AtmTransformer dt = new AtmTransformer (clazz.getName (), classLoader); instrumentation.addTransformer (dt, true); prøv {instrumentation.retransformClasses (clazz); } catch (Exception ex) {throw new RuntimeException ("Transform failed for: [" + clazz.getName () + "]", ex); }}

Med dette ut av veien, la oss definere transformatoren for MyAtm klasse.

5.2. Definere vår Transformator

En klassetransformator må implementere ClassFileTransformer og implementere transformasjonsmetoden.

Vi bruker Javassist til å legge til byte-kode til MyAtm klasse og legg til en logg med total ATW-uttakstransaksjonstid:

offentlig klasse AtmTransformer implementerer ClassFileTransformer {@Override public byte [] transform (ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte [] classfileBuffer) {byte [] byteCode = classfileBuffer; Streng finalTargetClassName = this.targetClassName .replaceAll ("\.", "/"); hvis (! className.equals (finalTargetClassName)) {return byteCode; } hvis (className.equals (finalTargetClassName) && loader.equals (targetClassLoader)) {LOGGER.info ("[Agent] Transforming class MyAtm"); prøv {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 = ny 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 (\" [Application] Tilbaketrekksoperasjon fullført på: "+" \ "+ opTime + \" sekunder! \ ");"); m.insertAfter (endBlock.toString ()); byteCode = cc.toBytecode (); cc.detach (); } fange (NotFoundException | CannotCompileException | IOException e) {LOGGER.error ("Unntak", e); }} returner byteCode; }}

5.3. Opprette en agentmanifestfil

Til slutt, for å få en fungerende Java-agent, trenger vi en manifestfil med et par attributter.

Derfor kan vi finne den fullstendige listen over manifestattributter i den offisielle dokumentasjonen for instrumentasjonspakken.

I den endelige Java agent jar-filen vil vi legge til følgende linjer i manifestfilen:

Agent-klasse: com.baeldung.instrumentation.agent.MyInstrumentationAgent Can-Redefine-Classes: true Can-Retransform-Classes: true Premain-Class: com.baeldung.instrumentation.agent.MyInstrumentationAgent

Vår Java-instrumenteringsagent er nå komplett. For å kjøre den, se Laste inn en Java Agent-seksjon i denne artikkelen.

6. Konklusjon

I denne artikkelen snakket vi om Java Instrumentation API. Vi så på hvordan man laster en Java-agent inn i en JVM både statisk og dynamisk.

Vi så også på hvordan vi ville gå frem for å lage vår egen Java-agent fra bunnen av.

Som alltid kan den fulle implementeringen av eksemplet finnes på Github.


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