Einführung in Javassist

1. Übersicht

In diesem Artikel werden wir uns die Javasisst- Bibliothek (Java Programming Assistant) ansehen .

Einfach ausgedrückt, diese Bibliothek vereinfacht die Bearbeitung von Java-Bytecode mithilfe einer API auf hoher Ebene als die im JDK.

2. Maven-Abhängigkeit

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

 org.javassist javassist ${javaassist.version}   3.21.0-GA 

3. Was ist der Bytecode?

Auf sehr hohem Niveau 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 Montageanweisungen auf Maschinenebene.

Nehmen wir an, wir haben eine Punktklasse :

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 die Point.class- Datei mit dem Bytecode erstellt. Wir können den Bytecode dieser Klasse sehen, indem wir den Befehl javap ausführen :

javap -c Point.class

Dadurch wird die folgende Ausgabe gedruckt:

public class com.baeldung.javasisst.Point { public com.baeldung.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. eine große Anzahl von ihnen ist verfügbar.

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

  • Die Anweisung aload_0 lä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 Feld x unseres Objekts. Alle Operationen sind für Feld y analog
  • Die letzte Anweisung ist eine Rückgabe

Jede Zeile Java-Code wird mit den richtigen Anweisungen zu Bytecode kompiliert. Die Javassist-Bibliothek macht die Bearbeitung dieses Bytecodes relativ einfach.

4. Generieren einer Java-Klasse

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

Angenommen , wir möchten eine JavassistGeneratedClass- Klasse generieren , die eine java.lang.Cloneable- Schnittstelle implementiert . Wir möchten, dass diese Klasse ein ID- Feld vom Typ int hat . Die ClassFile wird verwendet, um eine neue Klassendatei zu erstellen, und FieldInfo wird verwendet, um einer Klasse ein neues Feld hinzuzufügen :

ClassFile cf = new ClassFile( false, "com.baeldung.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 eine JavassistGeneratedClass.class erstellt haben, können wir behaupten, dass sie tatsächlich ein ID- 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 wir ein CodeAttribute einer bestimmten Methode der Klasse abrufen . Dann können wir einen CodeIterator veranlassen , alle Bytecode-Anweisungen dieser Methode zu durchlaufen .

Laden wir alle Bytecode-Anweisungen der move () -Methode der Point- Klasse:

ClassPool cp = ClassPool.getDefault(); ClassFile cf = cp.get("com.baeldung.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 der move () -Methode anzeigen, indem wir Bytecodes zur Liste der Operationen aggregieren, wie in der obigen Zusicherung gezeigt.

6. Hinzufügen von Feldern zum vorhandenen Klassenbytecode

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

ClassFile cf = ClassPool.getDefault() .get("com.baeldung.javasisst.Point").getClassFile(); FieldInfo f = new FieldInfo(cf.getConstPool(), "id", "I"); f.setAccessFlags(AccessFlag.PUBLIC); cf.addField(f); 

Wir können Reflection verwenden, um zu überprüfen, ob das ID- Feld in der Point- Klasse vorhanden 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 eine addInvokespecial () -Methode verwenden.

Und wir können einen parameterlosen Konstruktor hinzufügen, indem wir a aufrufen Methode aus der Klasse java.lang.Object :

ClassFile cf = ClassPool.getDefault() .get("com.baeldung.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 das Vorhandensein des neu erstellten Konstruktors überprüfen, 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 Manipulation von Bytecodes 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 im GitHub-Projekt - dies ist ein Maven-Projekt, daher sollte es einfach zu importieren und auszuführen sein, wie es ist.