Gids voor Java-instrumentatie

1. Inleiding

In deze tutorial gaan we het hebben over Java Instrumentation API. Het biedt de mogelijkheid om byte-code toe te voegen aan bestaande gecompileerde Java-klassen.

We zullen ook praten over Java-agents en hoe we ze gebruiken om onze code te instrumenteren.

2. Installatie

In het hele artikel zullen we een app bouwen met behulp van instrumentatie.

Onze applicatie zal uit twee modules bestaan:

  1. Een pinautomaat-app waarmee we geld kunnen opnemen
  2. En een Java-agent waarmee we de prestaties van onze geldautomaat kunnen meten door de bestede tijd te meten

De Java-agent zal de ATM-byte-code wijzigen, zodat we de opnametijd kunnen meten zonder de ATM-app te hoeven wijzigen.

Ons project zal de volgende structuur hebben:

com.baeldung.instrumentation base 1.0.0 pom-agenttoepassing 

Laten we, voordat we teveel ingaan op de details van instrumentatie, eens kijken wat een Java-agent is.

3. Wat is een Java-agent

Over het algemeen is een Java-agent slechts een speciaal vervaardigd jar-bestand. Het maakt gebruik van de Instrumentation API die de JVM biedt om bestaande bytecode te wijzigen die in een JVM is geladen.

Om een ​​agent te laten werken, moeten we twee methoden definiëren:

  • premain - laadt de agent statisch met de parameter -javaagent bij het opstarten van JVM
  • agentmain - zal de agent dynamisch in de JVM laden met behulp van de Java Attach API

Een interessant concept om in gedachten te houden is dat een JVM-implementatie, zoals Oracle, OpenJDK en andere, een mechanisme kan bieden om agents dynamisch te starten, maar het is geen vereiste.

Laten we eerst eens kijken hoe we een bestaande Java-agent zouden gebruiken.

Daarna zullen we bekijken hoe we er een kunnen maken vanaf het begin om de functionaliteit toe te voegen die we nodig hebben in onze byte-code.

4. Het laden van een Java-agent

Om de Java-agent te kunnen gebruiken, moeten we deze eerst laden.

We hebben twee soorten lading:

  • statisch - maakt gebruik van de premain om de agent te laden met de optie -javaagent
  • dynamisch - maakt gebruik van de agentmain om de agent in de JVM te laden met behulp van de Java Attach API

Vervolgens bekijken we elk type belasting en leggen we uit hoe het werkt.

4.1. Statische belasting

Het laden van een Java-agent bij het opstarten van een applicatie wordt statische belasting genoemd. Statische belasting wijzigt de bytecode tijdens het opstarten voordat een code wordt uitgevoerd.

Houd er rekening mee dat de statische belasting de premain methode, die wordt uitgevoerd voordat een applicatiecode wordt uitgevoerd, om deze te laten werken, kunnen we het volgende uitvoeren:

java -javaagent: agent.jar -jar applicatie.jar

Het is belangrijk op te merken dat we altijd de -javaagent parameter voor de -pot parameter.

Hieronder staan ​​de logboeken voor onze opdracht:

22: 24: 39.296 [main] INFO - [Agent] In premain-methode 22: 24: 39.300 [main] INFO - [Agent] Transformatieklasse MyAtm 22: 24: 39.407 [main] INFO - [Applicatie] ATM-applicatie 22 starten: 24: 41.409 [main] INFO - [Applicatie] Succesvolle intrekking van [7] eenheden! 22: 24: 41.410 [main] INFO - [Applicatie] Intrekking voltooid in: 2 seconden! 22: 24: 53.411 [main] INFO - [Applicatie] Succesvolle terugtrekking van [8] eenheden! 22: 24: 53.411 [main] INFO - [Applicatie] Intrekking voltooid in: 2 seconden!

We kunnen zien wanneer de premain methode uitgevoerd en wanneer MyAtm klasse is getransformeerd. We zien ook de twee logboeken voor geldopnames van geldautomaten die de tijd bevatten die nodig was om elke bewerking te voltooien.

Onthoud dat we in onze oorspronkelijke applicatie deze voltooiingstijd voor een transactie niet hadden, deze werd toegevoegd door onze Java-agent.

4.2. Dynamische belasting

De procedure voor het laden van een Java-agent in een reeds draaiende JVM wordt dynamische belasting genoemd. De agent is gekoppeld met behulp van de Java Attach API.

Een complexer scenario is wanneer we onze ATM-applicatie al in productie hebben en we de totale tijd van transacties dynamisch willen optellen zonder downtime voor onze applicatie.

Laten we een klein stukje code schrijven om precies dat te doen en we zullen deze klasse bellen AgentLoader. Voor de eenvoud plaatsen we deze klasse in het jar-bestand van de toepassing. Ons toepassingsjar-bestand kan dus zowel onze toepassing starten als onze agent aan de ATM-toepassing koppelen:

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

Nu we onze hebben AgentLoader, we starten onze applicatie en zorgen ervoor dat we in de pauze van tien seconden tussen transacties onze Java-agent dynamisch koppelen met behulp van de AgentLoader.

Laten we ook de lijm toevoegen waarmee we de applicatie kunnen starten of de agent kunnen laden.

We zullen deze klas bellen Launcher en het zal onze belangrijkste jar-bestandsklasse zijn:

public class Launcher {public static void main (String [] args) gooit uitzondering {if (args [0] .equals ("StartMyAtmApplication")) {new MyAtmApplication (). run (args); } else if (args [0] .equals ("LoadAgent")) {nieuwe AgentLoader (). run (args); }}}

De applicatie starten

java -jar application.jar StartMyAtmApplication 22: 44: 21.154 [main] INFO - [Applicatie] ATM-applicatie starten 22: 44: 23.157 [main] INFO - [Applicatie] Succesvolle intrekking van [7] eenheden!

Java-agent koppelen

Na de eerste bewerking koppelen we de Java-agent aan onze JVM:

java -jar application.jar LoadAgent 22: 44: 27.022 [main] INFO - Koppelen aan doel-JVM met PID: 6575 22: 44: 27.306 [main] INFO - Bijgevoegd aan doel-JVM en Java-agent met succes geladen 

Controleer de toepassingslogboeken

Nu we onze agent aan de JVM hebben gekoppeld, zullen we zien dat we de totale voltooiingstijd hebben voor de tweede opname van geldautomaten.

Dit betekent dat we onze functionaliteit on the fly hebben toegevoegd terwijl onze applicatie actief was:

22: 44: 27.229 [Luisteraar bijvoegen] INFO - [Agent] In agentmain-methode 22: 44: 27.230 [Luisteraar bijvoegen] INFO - [Agent] Transformatieklasse MyAtm 22: 44: 33.157 [hoofd] INFO - [Toepassing] Succesvolle intrekking van [8] eenheden! 22: 44: 33.157 [main] INFO - [Applicatie] Intrekking voltooid in: 2 seconden!

5. Een Java-agent maken

Laten we, nadat we hebben geleerd hoe we een agent moeten gebruiken, kijken hoe we er een kunnen maken. We zullen bekijken hoe Javassist kan worden gebruikt om bytecode te wijzigen en we zullen dit combineren met enkele instrumentatie-API-methoden.

Omdat een Java-agent gebruik maakt van de Java Instrumentation API, laten we, voordat we te diep ingaan op het maken van onze agent, eerst eens kijken naar enkele van de meest gebruikte methoden in deze API en een korte beschrijving van wat ze doen:

  • addTransformer - voegt een transformator toe aan de instrumentatiemotor
  • getAllLoadedClasses - geeft een array terug van alle klassen die momenteel zijn geladen door de JVM
  • retransformClasses - vergemakkelijkt de instrumentatie van reeds geladen klassen door byte-code toe te voegen
  • removeTransformer - meldt de meegeleverde transformator af
  • herdefinieer Klassen - herdefinieer de meegeleverde set van klassen met behulp van de meegeleverde klassebestanden, wat betekent dat de klasse volledig zal worden vervangen, niet gewijzigd zoals bij retransformClasses

5.1. Maak het Premain en Agentmain Methoden

We weten dat elke Java-agent ten minste één van de premain of agentmain methoden. De laatste wordt gebruikt voor dynamische belasting, terwijl de eerste wordt gebruikt om een ​​Java-agent statisch in een JVM te laden.

Laten we ze allebei in onze agent definiëren, zodat we deze agent zowel statisch als dynamisch kunnen laden:

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

Bij elke methode declareren we de klasse die we willen wijzigen en graven we vervolgens naar beneden om die klasse te transformeren met behulp van de transformClass methode.

Hieronder staat de code voor de transformClass methode die we hebben gedefinieerd om ons te helpen transformeren MyAtm klasse.

In deze methode zoeken we de klasse die we willen transformeren en gebruiken we de transformeren methode. We voegen ook de transformator toe aan de instrumentatie-engine:

privé statische ongeldige transformClass (String className, instrumentatie-instrumentatie) {Class targetCls = null; ClassLoader targetClassLoader = null; // kijk of we de klasse kunnen krijgen met forName, probeer {targetCls = Class.forName (className); targetClassLoader = targetCls.getClassLoader (); transform (targetCls, targetClassLoader, instrumentatie); terugkeren; } catch (Exception ex) {LOGGER.error ("Class [{}] niet gevonden met Class.forName"); } // herhaal anders alle geladen klassen en vind wat we zoeken voor (Class clazz: instrumentation.getAllLoadedClasses ()) {if (clazz.getName (). equals (className)) {targetCls = clazz; targetClassLoader = targetCls.getClassLoader (); transform (targetCls, targetClassLoader, instrumentatie); terugkeren; }} throw new RuntimeException ("Kan klasse [" + className + "]" niet vinden); } privé statische ongeldige transformatie (Class clazz, ClassLoader classLoader, instrumentatie-instrumentatie) {AtmTransformer dt = nieuwe AtmTransformer (clazz.getName (), classLoader); instrumentation.addTransformer (dt, true); probeer {instrumentation.retransformClasses (clazz); } catch (Exception ex) {throw new RuntimeException ("Transformatie mislukt voor: [" + clazz.getName () + "]", ex); }}

Laten we, met dit uit de weg, de transformator definiëren voor MyAtm klasse.

5.2. Onze Transformator

Een klasse-transformator moet implementeren ClassFileTransformer en implementeer de transformatiemethode.

We gebruiken Javassist om byte-code toe te voegen aan MyAtm klasse en voeg een logboek toe met de totale ATW-opnametransactietijd:

public class AtmTransformer implementeert ClassFileTransformer {@Override public byte [] transform (ClassLoader loader, String className, 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 ("[Agent] Transforming class MyAtm"); probeer {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 = nieuwe 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 (\" [Applicatie] Uitbetalingsoperatie voltooid in: "+" \ "+ opTime + \" seconden! \ ");"); m.insertAfter (endBlock.toString ()); byteCode = cc.toBytecode (); cc.detach (); } catch (NotFoundException | CannotCompileException | IOException e) {LOGGER.error ("Exception", e); }} retour byteCode; }}

5.3. Een agentmanifestbestand maken

Ten slotte, om een ​​werkende Java-agent te krijgen, hebben we een manifestbestand met een aantal attributen nodig.

Daarom kunnen we de volledige lijst met manifestattributen vinden in de officiële documentatie van het Instrumentation Package.

In het uiteindelijke jar-bestand van de Java-agent voegen we de volgende regels toe aan het manifestbestand:

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

Onze Java-instrumentatieagent is nu voltooid. Raadpleeg het gedeelte Een Java-agent laden in dit artikel om het uit te voeren.

6. Conclusie

In dit artikel hebben we het gehad over de Java Instrumentation API. We hebben gekeken hoe een Java-agent zowel statisch als dynamisch in een JVM kan worden geladen.

We hebben ook gekeken hoe we onze eigen Java-agent vanaf nul zouden kunnen maken.

Zoals altijd is de volledige implementatie van het voorbeeld te vinden op Github.