Einführung in Javassist

Einführung in Javassist

1. Überblick

In diesem Artikel werden wir uns die BibliothekJavasisst (Java Programming Assistant)ansehen.

Einfach ausgedrückt vereinfacht diese Bibliothek das Manipulieren von Java-Bytecode durch Verwendung einer API auf höherer Ebene als der im JDK.

2. Maven-Abhängigkeit

Um die Javassist-Bibliothek zu unserem Projekt hinzuzufügen, müssen wirjavassist zu unserem pom hinzufügen:


    org.javassist
    javassist
    ${javaassist.version}



    3.21.0-GA

3. Was ist der Bytecode?

Auf einer sehr hohen Ebene jede Java-Klasse, die in einem Nur-Text-Format geschrieben und zu Bytecode kompiliert ist - ein Befehlssatz, der von der Java Virtual Machine verarbeitet werden kann. Die JVM übersetzt Bytecode-Anweisungen in Assembly-Anweisungen auf Maschinenebene.

Nehmen wir an, wir haben einePoint-Klasse:

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
}

Nach der Kompilierung wird diePoint.class-Datei mit dem Bytecode erstellt. Wir können den Bytecode dieser Klasse sehen, indem wir den Befehljavap ausführen:

javap -c Point.class

Dadurch wird die folgende Ausgabe gedruckt:

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
}

Alle diese Anweisungen werden von der Java-Sprache angegeben. a large number of them are available.

Lassen Sie uns die Bytecode-Anweisungen dermove()-Methode analysieren:

  • Der Befehlaload_0lädt eine Referenz aus der lokalen Variablen 0 auf den Stapel

  • iload_1 lädt einen int-Wert aus der lokalen Variablen 1

  • putfield setzt ein Feldx unseres Objekts. Alle Operationen sind analog für Feldy

  • Die letzte Anweisung ist areturn

Jede Zeile des Java-Codes wird mit entsprechenden Anweisungen zu Bytecode kompiliert. Die Javassist-Bibliothek macht die Manipulation dieses Bytecodes relativ einfach.

4. Generieren einer Java-Klasse

Die Javassist-Bibliothek kann zum Generieren neuer Java-Klassendateien verwendet werden.

Angenommen, wir möchten eineJavassistGeneratedClass-Klasse generieren, die einejava.lang.Cloneable-Schnittstelle implementiert. Wir möchten, dass diese Klasse einid-Feld vomint-Typ. hat. MitClassFile wird eine neue Klassendatei erstellt und mitFieldInfo wird a hinzugefügt neues Feld zu einer Klasse:

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

Nachdem wir einJavassistGeneratedClass.class erstellt haben, können wir behaupten, dass es tatsächlich einid-Feld hat:

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

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

5. Laden von Bytecode-Anweisungen der Klasse

Wenn wir Bytecode-Anweisungen einer bereits vorhandenen Klassenmethode laden möchten, können wirCodeAttribute einer bestimmten Methode der Klasse erhalten. Dann können wirCodeIterator erhalten, um alle Bytecode-Anweisungen dieser Methode zu durchlaufen.

Laden wir alle Bytecode-Anweisungen dermove()-Methode derPoint-Klasse:

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

Wir können alle Bytecode-Anweisungen dermove()-Methode anzeigen, indem wir Bytecodes zur Liste der Operationen aggregieren, wie in der obigen Aussage gezeigt.

6. Hinzufügen von Feldern zum vorhandenen Klassenbytecode

Angenommen, wir möchten dem Bytecode der vorhandenen Klasse ein Feld vom Typinthinzufügen. Wir können diese Klasse mitClassPoll laden und ein Feld hinzufügen:

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

Wir können die Reflexion verwenden, um zu überprüfen, ob das Feldidin der KlassePointvorhanden ist:

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. Hinzufügen eines Konstruktors zum Klassenbytecode

Wir können der vorhandenen Klasse, die in einem der vorherigen Beispiele erwähnt wurde, einen Konstruktor hinzufügen, indem wir eineaddInvokespecial()-Methode verwenden.

Und wir können einen parameterlosen Konstruktor hinzufügen, indem wir die Methode<init>aus der Klassejava.lang.Objectaufrufen:

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

Wir können überprüfen, ob der neu erstellte Konstruktor vorhanden ist, indem wir den Bytecode durchlaufen:

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. Fazit

In diesem Artikel haben wir die Javassist-Bibliothek vorgestellt, um die Bytecode-Manipulation zu vereinfachen.

Wir haben uns auf die Kernfunktionen konzentriert und eine Klassendatei aus Java-Code generiert. Wir haben auch einige Bytecode-Manipulationen an einer bereits erstellten Java-Klasse vorgenommen.

Die Implementierung all dieser Beispiele und Codefragmente finden Sie inGitHub project - dies ist ein Maven-Projekt, daher sollte es einfach zu importieren und auszuführen sein, wie es ist.