Vejledning til Java-instrumentering

1. Introduktion

I denne vejledning skal vi tale om Java Instrumentation API. Det giver mulighed for at tilføje byte-kode til eksisterende kompilerede Java-klasser.

Vi vil også tale om java-agenter, og hvordan vi bruger dem til at instrumentere vores kode.

2. Opsætning

I hele artiklen bygger vi en app ved hjælp af instrumentering.

Vores ansøgning vil bestå af to moduler:

  1. En pengeautomatapp, der giver os mulighed for at hæve penge
  2. Og en Java-agent, der giver os mulighed for at måle udførelsen af ​​vores pengeautomat ved at måle den investerede tid på at bruge penge

Java-agenten vil ændre ATM-byte-koden, så vi kan måle tilbagetrækningstid uden at skulle ændre ATM-appen.

Vores projekt vil have følgende struktur:

com.baeldung.instrumentation base 1.0.0 applikation til pom-agent 

Før vi går for meget ind i detaljerne i instrumentering, lad os se, hvad en java-agent er.

3. Hvad er en Java-agent

Generelt er en java-agent bare en specielt fremstillet jar-fil. Det bruger Instrumentation API, som JVM leverer til at ændre eksisterende byte-kode, der er indlæst i en JVM.

For at en agent skal arbejde, skal vi definere to metoder:

  • premain - indlæser agenten statisk ved hjælp af -javaagent-parameteren ved JVM-opstart
  • agenthoved - vil dynamisk indlæse agenten i JVM ved hjælp af Java Attach API

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

Lad os først se, hvordan vi bruger en eksisterende Java-agent.

Derefter ser vi på, hvordan vi kan oprette en fra bunden for at tilføje den funktionalitet, vi har brug for i vores byte-kode.

4. Indlæsning af en Java-agent

For at kunne bruge Java-agenten skal vi først indlæse den.

Vi har to typer belastning:

  • statisk - gør brug af premain for at indlæse agenten ved hjælp af -javaagent-indstillingen
  • dynamisk - gør brug af agenthoved at indlæse agenten i JVM ved hjælp af Java Attach API

Dernæst ser vi på hver type belastning og forklarer, hvordan den fungerer.

4.1. Statisk belastning

Indlæsning af en Java-agent ved opstart af applikationen kaldes statisk belastning. Statisk belastning ændrer byte-koden ved opstartstid, før en kode udføres.

Husk, at den statiske belastning bruger premain metode, som kører, før en applikationskode kører, for at få den til at køre, kan vi udføre:

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

Det er vigtigt at bemærke, at vi altid skal sætte -javaagent parameter før -krukke parameter.

Nedenfor er logfiler til vores kommando:

22: 24: 39.296 [main] INFO - [Agent] I premain-metode 22: 24: 39.300 [main] INFO - [Agent] Transforming class MyAtm 22: 24: 39.407 [main] INFO - [Application] Starter ATM-applikation 22: 24: 41.409 [main] INFO - [Application] Vellykket tilbagetrækning af [7] enheder! 22: 24: 41.410 [main] INFO - [Application] Tilbagetrækning afsluttet på: 2 sekunder! 22: 24: 53.411 [main] INFO - [Application] Vellykket tilbagetrækning af [8] enheder! 22: 24: 53.411 [main] INFO - [Application] Tilbagetrækning afsluttet på: 2 sekunder!

Vi kan se, hvornår premain metoden løb og hvornår MyAtm klasse blev forvandlet. Vi ser også de to ATM-tilbagekøbslogfiler, der indeholder den tid, det tog hver operation at gennemføre.

Husk, at vi i vores oprindelige ansøgning ikke havde afsluttet dette tidspunkt for en transaktion, det blev tilføjet af vores Java-agent.

4.2. Dynamisk belastning

Proceduren for at indlæse en Java-agent i en allerede kørende JVM kaldes dynamisk belastning. Agenten er tilknyttet ved hjælp af Java Attach API.

Et mere komplekst scenario er, når vi allerede har vores ATM-applikation kørende i produktion, og vi vil tilføje den samlede tid for transaktioner dynamisk uden nedetid for vores applikation.

Lad os skrive et lille stykke kode for at gøre netop det, og vi kalder denne klasse AgentLoader. For enkelheds skyld anbringer vi denne klasse i applikations jar-filen. Så vores applikations jar-fil kan både starte vores ansøgning og vedhæfte vores agent til ATM-applikationen:

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

Nu hvor vi har vores AgentLoader, vi starter vores ansøgning og sørger for, at vi i ti-sekunders pause mellem transaktionerne vedhæfter vores Java-agent dynamisk ved hjælp af AgentLoader.

Lad os også tilføje den lim, der giver os mulighed for enten at starte applikationen eller indlæse agenten.

Vi kalder denne klasse Launcher og det vil være vores vigtigste jar-filklasse:

public class Launcher {public static void main (String [] args) throw Exception {if (args [0] .equals ("StartMyAtmApplication")) {new MyAtmApplication (). run (args); } ellers hvis (args [0] .equals ("LoadAgent")) {new AgentLoader (). run (args); }}}

Start af applikationen

java -jar application.jar StartMyAtmApplication 22: 44: 21.154 [main] INFO - [Application] Start af ATM-applikation 22: 44: 23.157 [main] INFO - [Application] Vellykket tilbagetrækning af [7] enheder!

Vedhæftning af Java Agent

Efter den første operation vedhæfter vi java-agenten til vores JVM:

java -jar application.jar LoadAgent 22: 44: 27.022 [main] INFO - Vedhæftning til mål-JVM med PID: 6575 22: 44: 27.306 [main] INFO - Vedhæftet til mål-JVM og indlæst Java-agent med succes 

Kontroller applikationslogfiler

Nu hvor vi knyttede vores agent til JVM, ser vi, at vi har den samlede afslutningstid for den anden ATM-tilbagetrækningsoperation.

Dette betyder, at vi tilføjede vores funktionalitet på farten, mens vores applikation kørte:

22: 44: 27.229 [Vedhæft lytter] INFO - [Agent] I agenthovedmetode 22: 44: 27.230 [Vedhæft lytter] INFO - [Agent] Transformeringsklasse MyAtm 22: 44: 33.157 [main] INFO - [Application] Vellykket tilbagetrækning af [8] enheder! 22: 44: 33.157 [main] INFO - [Application] Tilbagetrækning afsluttet på: 2 sekunder!

5. Oprettelse af en Java-agent

Efter at have lært at bruge en agent, lad os se, hvordan vi kan oprette en. Vi ser på, hvordan du bruger Javassist til at ændre byte-kode, og vi kombinerer dette med nogle instrumenterings-API-metoder.

Da en java-agent gør brug af Java Instrumentation API, lad os se nogle af de mest anvendte metoder i denne API og en kort beskrivelse af, hvad de gør, før de går for dybt ind i oprettelsen af ​​vores agent:

  • addTransformer - tilføjer en transformer til instrumentationsmotoren
  • getAllLoadedClasses - returnerer en matrix med alle klasser, der aktuelt er indlæst af JVM
  • retransformClasses - letter instrumenteringen af ​​allerede indlæste klasser ved at tilføje byte-kode
  • Fjern Transformer - afregistrerer den medfølgende transformer
  • omdefinere briller - omdefiner det medfølgende klassesæt ved hjælp af de medfølgende klassefiler, hvilket betyder, at klassen vil blive fuldstændigt udskiftet, ikke ændret som med retransformClasses

5.1. Opret Premain og Agenthoved Metoder

Vi ved, at enhver Java-agent har brug for mindst en af premain eller agenthoved metoder. Sidstnævnte bruges til dynamisk belastning, mens førstnævnte bruges til statisk at indlæse et Java-agent i en JVM.

Lad os definere dem begge i vores agent, så vi er i stand til at indlæse denne agent 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 den klasse, som vi vil ændre, og graver derefter ned for at omdanne den klasse ved hjælp af transformClass metode.

Nedenfor er koden til transformClass metode, som vi definerede for at hjælpe os med at transformere MyAtm klasse.

I denne metode finder vi den klasse, vi vil transformere, og ved hjælp af transformere metode. Vi tilføjer også transformeren til instrumentationsmotoren:

privat statisk ugyldig transformClass (String className, Instrumentation instrumentering) {Class targetCls = null; ClassLoader targetClassLoader = null; // se om vi kan få klassen ved hjælp af forName, prøv {targetCls = Class.forName (className); targetClassLoader = targetCls.getClassLoader (); transformere (targetCls, targetClassLoader, instrumentering); Vend tilbage; } fange (Undtagelse ex) {LOGGER.error ("Klasse [{}] ikke fundet med Class.forName"); } // ellers gentager alle indlæste klasser og finder det, vi ønsker (Class clazz: instrumentation.getAllLoadedClasses ()) {if (clazz.getName (). er lig med (className)) {targetCls = clazz; targetClassLoader = targetCls.getClassLoader (); transformere (targetCls, targetClassLoader, instrumentering); Vend tilbage; }} smid ny RuntimeException ("Kunne ikke finde klasse [" + className + "]"); } privat statisk ugyldig transformation (Class clazz, ClassLoader classLoader, Instrumentation instrumentering) {AtmTransformer dt = ny AtmTransformer (clazz.getName (), classLoader); instrumentation.addTransformer (dt, sand); prøv {instrumentation.retransformClasses (clazz); } catch (Exception ex) {throw new RuntimeException ("Transform failed for: [" + clazz.getName () + "]", ex); }}

Med dette ude af vejen, lad os definere transformeren til MyAtm klasse.

5.2. Definere vores Transformer

En klassetransformator skal implementere ClassFileTransformer og implementere transformeringsmetoden.

Vi bruger Javassist til at føje byte-kode til MyAtm klasse og tilføj en log med den samlede ATW-tilbagetrækningstransaktionstid:

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] Tilbagetrækning afsluttet på: "+" \ "+ opTime + \" sekunder! \ ");"); m.insertAfter (endBlock.toString ()); byteCode = cc.toBytecode (); cc.detach (); } fange (NotFoundException | CannotCompileException | IOException e) {LOGGER.error ("Undtagelse", e); }} returner byteCode; }}

5.3. Oprettelse af en agentmanifestfil

Endelig har vi brug for en manifestfil med et par attributter for at få en fungerende Java-agent.

Derfor kan vi finde den fulde liste over manifestattributter i den officielle dokumentation til Instrumentation Package.

I den sidste Java-agent jar-fil tilføjer vi følgende linjer til manifestfilen:

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

Vores Java-instrumenteringsagent er nu komplet. For at køre det henvises til afsnittet Indlæsning af en Java Agent i denne artikel.

6. Konklusion

I denne artikel talte vi om Java Instrumentation API. Vi så på, hvordan man indlæser en Java-agent i en JVM både statisk og dynamisk.

Vi kiggede også på, hvordan vi ville gå til at oprette vores egen Java-agent fra bunden.

Som altid kan den fulde implementering af eksemplet findes på Github.


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