Introduction à Javassist

Introduction à Javassist

1. Vue d'ensemble

Dans cet article, nous examinerons la bibliothèqueJavasisst (Java Programming Assistant).

En termes simples, cette bibliothèque simplifie le processus de manipulation du bytecode Java en utilisant une API de haut niveau que celle contenue dans le JDK.

2. Dépendance Maven

Pour ajouter la bibliothèque Javassist à notre projet, nous devons ajouterjavassist dans notre pom:


    org.javassist
    javassist
    ${javaassist.version}



    3.21.0-GA

3. Quel est le bytecode?

À un niveau très élevé, chaque classe Java écrite dans un format de texte brut et compilée en bytecode - un jeu d'instructions pouvant être traité par la machine virtuelle Java. La machine virtuelle Java convertit les instructions de bytecode en instructions d'assemblage au niveau de la machine.

Disons que nous avons une classePoint:

public class Point {
    private int x;
    private int y;

    public void move(int x, int y) {
        this.x = x;
        this.y = y;
    }

    // standard constructors/getters/setters
}

Après compilation, le fichierPoint.class contenant le bytecode sera créé. Nous pouvons voir le bytecode de cette classe en exécutant la commandejavap:

javap -c Point.class

Ceci imprimera la sortie suivante:

public class com.example.javasisst.Point {
  public com.example.javasisst.Point(int, int);
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."":()V
       4: aload_0
       5: iload_1
       6: putfield      #2                  // Field x:I
       9: aload_0
      10: iload_2
      11: putfield      #3                  // Field y:I
      14: return

  public void move(int, int);
    Code:
       0: aload_0
       1: iload_1
       2: putfield      #2                  // Field x:I
       5: aload_0
       6: iload_2
       7: putfield      #3                  // Field y:I
      10: return
}

Toutes ces instructions sont spécifiées par le langage Java; a large number of them are available.

Analysons les instructions bytecode de la méthodemove():

  • L'instructionaload_0 charge une référence sur la pile à partir de la variable locale 0

  • iload_1 charge une valeur int à partir de la variable locale 1

  • putfield définit un champx de notre objet. Toutes les opérations sont analogiques pour le champy

  • La dernière instruction est unreturn

Chaque ligne de code Java est compilée en bytecode avec les instructions appropriées. La bibliothèque Javassist facilite relativement la manipulation de ce bytecode.

4. Générer une classe Java

La bibliothèque Javassist peut être utilisée pour générer de nouveaux fichiers de classe Java.

Disons que nous voulons générer une classeJavassistGeneratedClass qui implémente une interfacejava.lang.Cloneable. Nous voulons que cette classe ait un champid de typeint. LeClassFile est utilisé pour créer un nouveau fichier de classe etFieldInfo est utilisé pour ajouter un nouveau champ dans une classe:

ClassFile cf = new ClassFile(
  false, "com.example.JavassistGeneratedClass", null);
cf.setInterfaces(new String[] {"java.lang.Cloneable"});

FieldInfo f = new FieldInfo(cf.getConstPool(), "id", "I");
f.setAccessFlags(AccessFlag.PUBLIC);
cf.addField(f);

Après avoir créé unJavassistGeneratedClass.class, nous pouvons affirmer qu'il a en fait un champid:

ClassPool classPool = ClassPool.getDefault();
Field[] fields = classPool.makeClass(cf).toClass().getFields();

assertEquals(fields[0].getName(), "id");

5. Chargement des instructions Bytecode de la classe

Si nous voulons charger des instructions bytecode d'une méthode de classe déjà existante, nous pouvons obtenir unCodeAttribute d'une méthode spécifique de la classe. Ensuite, nous pouvons obtenir unCodeIterator pour itérer sur toutes les instructions de bytecode de cette méthode.

Chargons toutes les instructions bytecode de la méthodemove() de la classePoint:

ClassPool cp = ClassPool.getDefault();
ClassFile cf = cp.get("com.example.javasisst.Point")
  .getClassFile();
MethodInfo minfo = cf.getMethod("move");
CodeAttribute ca = minfo.getCodeAttribute();
CodeIterator ci = ca.iterator();

List operations = new LinkedList<>();
while (ci.hasNext()) {
    int index = ci.next();
    int op = ci.byteAt(index);
    operations.add(Mnemonic.OPCODE[op]);
}

assertEquals(operations,
  Arrays.asList(
  "aload_0",
  "iload_1",
  "putfield",
  "aload_0",
  "iload_2",
  "putfield",
  "return"));

Nous pouvons voir toutes les instructions de bytecode de la méthodemove() en agrégeant les bytecodes à la liste des opérations, comme indiqué dans l'assertion ci-dessus.

6. Ajout de champs à un bytecode de classe existant

Disons que nous voulons ajouter un champ de typeint au bytecode de la classe existante. Nous pouvons charger cette classe en utilisantClassPoll et y ajouter un champ:

ClassFile cf = ClassPool.getDefault()
  .get("com.example.javasisst.Point").getClassFile();

FieldInfo f = new FieldInfo(cf.getConstPool(), "id", "I");
f.setAccessFlags(AccessFlag.PUBLIC);
cf.addField(f);

Nous pouvons utiliser la réflexion pour vérifier que le champid existe sur la classePoint:

ClassPool classPool = ClassPool.getDefault();
Field[] fields = classPool.makeClass(cf).toClass().getFields();
List fieldsList = Stream.of(fields)
  .map(Field::getName)
  .collect(Collectors.toList());

assertTrue(fieldsList.contains("id"));

7. Ajout d'un constructeur au bytecode de classe

Nous pouvons ajouter un constructeur à la classe existante mentionnée dans l'un des exemples précédents en utilisant une méthodeaddInvokespecial().

Et nous pouvons ajouter un constructeur sans paramètre en appelant une méthode<init> de la classejava.lang.Object:

ClassFile cf = ClassPool.getDefault()
  .get("com.example.javasisst.Point").getClassFile();
Bytecode code = new Bytecode(cf.getConstPool());
code.addAload(0);
code.addInvokespecial("java/lang/Object", MethodInfo.nameInit, "()V");
code.addReturn(null);

MethodInfo minfo = new MethodInfo(
  cf.getConstPool(), MethodInfo.nameInit, "()V");
minfo.setCodeAttribute(code.toCodeAttribute());
cf.addMethod(minfo);

Nous pouvons vérifier la présence du constructeur nouvellement créé en itérant sur bytecode:

CodeIterator ci = code.toCodeAttribute().iterator();
List operations = new LinkedList<>();
while (ci.hasNext()) {
    int index = ci.next();
    int op = ci.byteAt(index);
    operations.add(Mnemonic.OPCODE[op]);
}

assertEquals(operations,
  Arrays.asList("aload_0", "invokespecial", "return"));

8. Conclusion

Dans cet article, nous avons présenté la bibliothèque Javassist, dans le but de faciliter la manipulation de code-octet.

Nous nous sommes concentrés sur les fonctionnalités principales et avons généré un fichier de classe à partir de code Java. nous avons également procédé à des manipulations de bytecode d’une classe Java déjà créée.

L'implémentation de tous ces exemples et extraits de code peut être trouvée dans leGitHub project - il s'agit d'un projet Maven, il devrait donc être facile à importer et à exécuter tel quel.