Guide de l'instrumentation Java

1. Introduction

Dans ce tutoriel, nous allons parler de l'API Java Instrumentation. Il offre la possibilité d'ajouter du code d'octet aux classes Java compilées existantes.

Nous parlerons également des agents java et de la manière dont nous les utilisons pour instrumenter notre code.

2. Configuration

Tout au long de l'article, nous allons créer une application à l'aide de l'instrumentation.

Notre application sera composée de deux modules:

  1. Une application ATM qui nous permet de retirer de l'argent
  2. Et un agent Java qui nous permettra de mesurer les performances de notre guichet automatique en mesurant le temps investi pour dépenser de l'argent

L'agent Java modifiera l'octet-code ATM nous permettant de mesurer le temps de retrait sans avoir à modifier l'application ATM.

Notre projet aura la structure suivante:

com.baeldung.instrumentation base 1.0.0 pom  agent application 

Avant d'entrer trop dans les détails de l'instrumentation, voyons ce qu'est un agent Java.

3. Qu'est-ce qu'un agent Java

En général, un agent Java est juste un fichier jar spécialement conçu. Il utilise l'API d'instrumentation fournie par la JVM pour modifier le code d'octet existant chargé dans une JVM.

Pour qu'un agent fonctionne, nous devons définir deux méthodes:

  • premain - chargera statiquement l'agent à l'aide du paramètre -javaagent au démarrage de la JVM
  • agentmain - chargera dynamiquement l'agent dans la JVM à l'aide de l'API Java Attach

Un concept intéressant à garder à l'esprit est qu'une implémentation JVM, comme Oracle, OpenJDK et autres, peut fournir un mécanisme pour démarrer les agents de manière dynamique, mais ce n'est pas une exigence.

Voyons d'abord comment nous utiliserions un agent Java existant.

Après cela, nous verrons comment nous pouvons en créer un à partir de zéro pour ajouter les fonctionnalités dont nous avons besoin dans notre byte-code.

4. Chargement d'un agent Java

Pour pouvoir utiliser l'agent Java, nous devons d'abord le charger.

Nous avons deux types de charge:

  • static - utilise le premain pour charger l'agent à l'aide de l'option -javaagent
  • dynamic - utilise l' agentmain pour charger l'agent dans la JVM à l'aide de l'API Java Attach

Ensuite, nous examinerons chaque type de charge et expliquerons son fonctionnement.

4.1. Charge statique

Le chargement d'un agent Java au démarrage de l'application s'appelle une charge statique. La charge statique modifie l'octet-code au moment du démarrage avant l'exécution de tout code.

Gardez à l'esprit que la charge statique utilise la méthode premain , qui s'exécutera avant l'exécution de tout code d'application, pour la faire fonctionner, nous pouvons l'exécuter:

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

Il est important de noter que nous devons toujours placer le paramètre - javaagent avant le paramètre - jar .

Voici les journaux de notre commande:

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] Starting ATM application 22:24:41.409 [main] INFO - [Application] Successful Withdrawal of [7] units! 22:24:41.410 [main] INFO - [Application] Withdrawal operation completed in:2 seconds! 22:24:53.411 [main] INFO - [Application] Successful Withdrawal of [8] units! 22:24:53.411 [main] INFO - [Application] Withdrawal operation completed in:2 seconds!

Nous pouvons voir quand la méthode premain s'est exécutée et quand la classe MyAtm a été transformée. Nous voyons également les deux journaux de transactions de retrait ATM qui contiennent le temps nécessaire à chaque opération pour se terminer.

N'oubliez pas que dans notre application d'origine, nous n'avions pas cette heure de fin pour une transaction, elle a été ajoutée par notre agent Java.

4.2. Charge dynamique

La procédure de chargement d'un agent Java dans une JVM déjà en cours d'exécution est appelée chargement dynamique. L'agent est attaché à l'aide de l'API Java Attach.

Un scénario plus complexe est lorsque notre application ATM est déjà en cours d'exécution en production et que nous voulons ajouter le temps total des transactions de manière dynamique sans temps d'arrêt pour notre application.

Écrivons un petit morceau de code pour faire exactement cela et nous appellerons cette classe AgentLoader. Pour plus de simplicité, nous allons mettre cette classe dans le fichier jar de l'application. Ainsi, notre fichier jar d'application peut à la fois démarrer notre application et attacher notre agent à l'application ATM:

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

Maintenant que nous avons notre AgentLoader , nous démarrons notre application en nous assurant que dans la pause de dix secondes entre les transactions, nous attacherons dynamiquement notre agent Java à l'aide de l' AgentLoader .

Ajoutons également la colle qui nous permettra soit de démarrer l'application, soit de charger l'agent.

Nous appellerons cette classe Launcher et ce sera notre principale classe de fichier jar:

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

Démarrage de l'application

java -jar application.jar StartMyAtmApplication 22:44:21.154 [main] INFO - [Application] Starting ATM application 22:44:23.157 [main] INFO - [Application] Successful Withdrawal of [7] units!

Attacher l'agent Java

Après la première opération, nous attachons l'agent java à notre JVM:

java -jar application.jar LoadAgent 22:44:27.022 [main] INFO - Attaching to target JVM with PID: 6575 22:44:27.306 [main] INFO - Attached to target JVM and loaded Java agent successfully 

Vérifier les journaux d'application

Maintenant que nous avons attaché notre agent à la JVM, nous verrons que nous avons le temps total d'achèvement pour la deuxième opération de retrait ATM.

Cela signifie que nous avons ajouté notre fonctionnalité à la volée, pendant que notre application était en cours d'exécution:

22:44:27.229 [Attach Listener] INFO - [Agent] In agentmain method 22:44:27.230 [Attach Listener] INFO - [Agent] Transforming class MyAtm 22:44:33.157 [main] INFO - [Application] Successful Withdrawal of [8] units! 22:44:33.157 [main] INFO - [Application] Withdrawal operation completed in:2 seconds!

5. Création d'un agent Java

Après avoir appris à utiliser un agent, voyons comment nous pouvons en créer un. Nous verrons comment utiliser Javassist pour modifier le code d'octet et nous le combinerons avec certaines méthodes d'API d'instrumentation.

Étant donné qu'un agent Java utilise l'API Java Instrumentation, avant de vous lancer dans la création de notre agent, voyons quelques-unes des méthodes les plus utilisées dans cette API et une brève description de ce qu'elles font:

  • addTransformer - ajoute un transformateur au moteur d'instrumentation
  • getAllLoadedClasses - renvoie un tableau de toutes les classes actuellement chargées par la JVM
  • retransformClasses - facilite l'instrumentation des classes déjà chargées en ajoutant du byte-code
  • removeTransformer - désenregistre le transformateur fourni
  • redefineClasses - redéfinit l'ensemble de classes fourni en utilisant les fichiers de classe fournis, ce qui signifie que la classe sera entièrement remplacée, pas modifiée comme avec retransformClasses

5.1. Create the Premain and Agentmain Methods

We know that every Java agent needs at least one of the premain or agentmain methods. The latter is used for dynamic load, while the former is used to statically load a java agent into a JVM.

Let's define both of them in our agent so that we're able to load this agent both statically and dynamically:

public static void 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); }

In each method, we declare the class that we want to change and then dig down to transform that class using the transformClass method.

Below is the code for the transformClass method that we defined to help us transform MyAtm class.

In this method, we find the class we want to transform and using the transform method. Also, we add the transformer to the instrumentation engine:

private static void transformClass( String className, Instrumentation instrumentation) { Class targetCls = null; ClassLoader targetClassLoader = null; // see if we can get the class using forName try { targetCls = Class.forName(className); targetClassLoader = targetCls.getClassLoader(); transform(targetCls, targetClassLoader, instrumentation); return; } catch (Exception ex) { LOGGER.error("Class [{}] not found with Class.forName"); } // otherwise iterate all loaded classes and find what we want for(Class clazz: instrumentation.getAllLoadedClasses()) { if(clazz.getName().equals(className)) { targetCls = clazz; targetClassLoader = targetCls.getClassLoader(); transform(targetCls, targetClassLoader, instrumentation); return; } } throw new RuntimeException( "Failed to find class [" + className + "]"); } private static void transform( Class clazz, ClassLoader classLoader, Instrumentation instrumentation) { AtmTransformer dt = new AtmTransformer( clazz.getName(), classLoader); instrumentation.addTransformer(dt, true); try { instrumentation.retransformClasses(clazz); } catch (Exception ex) { throw new RuntimeException( "Transform failed for: [" + clazz.getName() + "]", ex); } }

With this out of the way, let's define the transformer for MyAtm class.

5.2. Defining Our Transformer

A class transformer must implement ClassFileTransformer and implement the transform method.

We'll use Javassist to add byte-code to MyAtm class and add a log with the total ATW withdrawal transaction time:

public class AtmTransformer implements 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"); 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 = new 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] Withdrawal operation completed in:" + "\" + opTime + \" seconds!\");"); m.insertAfter(endBlock.toString()); byteCode = cc.toBytecode(); cc.detach(); } catch (NotFoundException | CannotCompileException | IOException e) { LOGGER.error("Exception", e); } } return byteCode; } }

5.3. Creating an Agent Manifest File

Finally, in order to get a working Java agent, we'll need a manifest file with a couple of attributes.

Par conséquent, nous pouvons trouver la liste complète des attributs manifestes dans la documentation officielle du paquet d'instrumentation.

Dans le fichier jar final de l'agent Java, nous ajouterons les lignes suivantes au fichier manifeste:

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

Notre agent d'instrumentation Java est maintenant terminé. Pour l'exécuter, reportez-vous à la section Chargement d'un agent Java de cet article.

6. Conclusion

Dans cet article, nous avons parlé de l'API Java Instrumentation. Nous avons examiné comment charger un agent Java dans une JVM à la fois de manière statique et dynamique.

Nous avons également examiné comment nous allions créer notre propre agent Java à partir de zéro.

Comme toujours, l'implémentation complète de l'exemple peut être trouvée sur Github.