Guide de l’instrumentation Java

Guide de l'instrumentation Java

1. introduction

Dans ce tutoriel, nous allons parler deJava Instrumentation API.. 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. Installer

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

Notre application comportera deux modules:

  1. Une application de guichet automatique 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 dans la dépense

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.example.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. It utilizes the Instrumentation API that the JVM provides to alter existing byte-code that is loaded in a 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 en utilisant lesJava Attach API

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

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 code d'octet.

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 lespremain pour charger l'agent à l'aide de l'option -javaagent

  • dynamic - utilise lesagentmain pour charger l'agent dans la JVM à l'aide desJava Attach API

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 est appelé chargement statique. Static load modifies the byte-code at startup time before any code is executed.

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

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

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

Vous trouverez ci-dessous 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éthodepremain a été exécutée et quandMyAtm class a été transformée. Nous voyons également les deux journaux des transactions de retrait ATM qui contiennent le temps nécessaire à chaque opération.

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

The procedure of loading a Java agent into an already running JVM is called dynamic load. L'agent est attaché à l'aide desJava Attach API.

Un scénario plus complexe se produit lorsque notre application ATM est déjà en production et que nous souhaitons 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 classeAgentLoader. Pour plus de simplicité, nous allons placer 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 nosAgentLoader, nous démarrons notre application en nous assurant que dans la pause de dix secondes entre les transactions, nous attacherons dynamiquement notre agent Java en utilisant lesAgentLoader.

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

Nous appellerons cette classeLauncher 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 machine virtuelle Java:

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 de la deuxième opération de retrait ATM.

Cela signifie que nous avons ajouté nos fonctionnalités à la volée pendant l'exécution de notre application:

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 examineronshow to use Javassist pour changer le code d'octet et nous combinerons cela avec certaines méthodes d'API d'instrumentation.

Puisqu'un agent java utilise lesJava Instrumentation API, 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 avecretransformClasses

5.1. Créer les méthodesPremain etAgentmain

Nous savons que chaque agent Java a besoin d'au moins une des méthodespremain ouagentmain. Ce dernier est utilisé pour le chargement dynamique, tandis que le premier est utilisé pour charger de manière statique un agent Java dans une machine virtuelle Java.

Définissons les deux dans notre agent afin que nous puissions charger cet agent à la fois de manière statique et dynamique:

public static void premain(
  String agentArgs, Instrumentation inst) {

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

    LOGGER.info("[Agent] In agentmain method");
    String className = "com.example.instrumentation.application.MyAtm";
    transformClass(className,inst);
}

Dans chaque méthode, nous déclarons la classe que nous voulons modifier, puis creusons pour transformer cette classe en utilisant la méthodetransformClass.

Vous trouverez ci-dessous le code de la méthodetransformClass que nous avons définie pour nous aider à transformer la classeMyAtm.

Dans cette méthode, nous trouvons la classe que nous voulons transformer et en utilisant la méthodetransform . De plus, nous ajoutons le transformateur au moteur d'instrumentation:

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);
    }
}

Avec cela à l'écart, définissons le transformateur pour la classeMyAtm.

5.2. Définition de nosTransformer

Un transformateur de classe doit implémenterClassFileTransformer et implémenter la méthode de transformation.

Nous utiliseronsJavassist pour ajouter du code d'octet à la classeMyAtm et ajouter un journal avec le temps total de transaction de retrait ATW:

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. Création d'un fichier de manifeste d'agent

Enfin, pour obtenir un agent Java fonctionnel, nous aurons besoin d'un fichier manifeste avec quelques attributs.

Par conséquent, nous pouvons trouver la liste complète des attributs manifestes dans la documentation officielle deInstrumentation Package.

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

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

Notre agent d'instrumentation Java est maintenant terminé. Pour l'exécuter, reportez-vous à la sectionLoading a Java Agent 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 machine virtuelle Java de manière statique et dynamique.

Nous avons également examiné la procédure à suivre pour créer notre propre agent Java à partir de rien.

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