Eine Anleitung zur Java-Bytecode-Manipulation mit ASM

1. Einführung

In diesem Artikel wird beschrieben, wie Sie die Bibliothek ASM zur Manipulation einer vorhandenen Java-Klasse ** verwenden, indem Sie Felder hinzufügen, Methoden hinzufügen und das Verhalten vorhandener Methoden ändern.

2. Abhängigkeiten

Wir müssen die ASM-Abhängigkeiten zu unserer pom.xml hinzufügen:

<dependency>
    <groupId>org.ow2.asm</groupId>
    <artifactId>asm</artifactId>
    <version>6.0</version>
</dependency>
<dependency>
    <groupId>org.ow2.asm</groupId>
    <artifactId>asm-util</artifactId>
    <version>6.0</version>
</dependency>

Wir können die neuesten Versionen von asm herunterladen. und asm-util von Maven Zentral

3. ASM-API-Grundlagen

Die ASM-API bietet zwei Arten der Interaktion mit Java-Klassen für die Umwandlung und Generierung: ereignisbasiert und baumbasiert.

3.1. Ereignisbasierte API

Diese API basiert stark auf dem Visitor -Muster und ähnelt dem SAX-Parsing-Modell der Verarbeitung von XML-Dokumenten. Es besteht im Kern aus folgenden Komponenten:

  • ClassReader - hilft beim Lesen von Klassendateien und ist der Anfang von

eine Klasse verwandeln ** ClassVisitor - stellt die Methoden bereit, mit denen die Klasse umgewandelt wird

nach dem Lesen der rohen Klassendateien ** ClassWriter - dient zur Ausgabe des Endprodukts der Klasse

Transformation

Im ClassVisitor haben wir alle Besuchermethoden, die wir verwenden, um die verschiedenen Komponenten (Felder, Methoden usw.) einer bestimmten Java-Klasse zu berühren. Wir tun dies, indem wir eine Unterklasse von ClassVisitor bereitstellen, um Änderungen in einer bestimmten Klasse zu implementieren.

Aufgrund der Notwendigkeit, die Integrität der Ausgabeklasse in Bezug auf Java-Konventionen und den resultierenden Bytecode zu wahren, erfordert diese Klasse eine strikte Reihenfolge, in der ihre Methoden aufgerufen werden müssen, um eine korrekte Ausgabe zu generieren.

Die ClassVisitor -Methoden in der ereignisbasierten API werden in der folgenden Reihenfolge aufgerufen:

visit
visitSource?
visitOuterClass?
( visitAnnotation | visitAttribute )**
( visitInnerClass | visitField | visitMethod )**
visitEnd

3.2. Baumbasierte API

Diese API ist eine objektorientiertere API und analog zum JAXB-Modell ** der Verarbeitung von XML-Dokumenten.

Sie basiert weiterhin auf der ereignisbasierten API, führt jedoch die ClassNode -Root-Klasse ein Diese Klasse dient als Einstieg in die Klassenstruktur.

4. Arbeiten mit der ereignisbasierten ASM-API

Wir werden die java.lang.Integer -Klasse mit ASM ändern. Und an dieser Stelle müssen wir ein grundlegendes Konzept verstehen: Die Klasse ClassVisitor enthält alle erforderlichen Besuchermethoden, um alle Teile einer Klasse zu erstellen oder zu modifizieren .

Wir müssen nur die Besuchermethode überschreiben, um unsere Änderungen umzusetzen. Beginnen wir mit dem Einrichten der Voraussetzungenkomponenten:

public class CustomClassWriter {

    static String className = "java.lang.Integer";
    static String cloneableInterface = "java/lang/Cloneable";
    ClassReader reader;
    ClassWriter writer;

    public CustomClassWriter() {
        reader = new ClassReader(className);
        writer = new ClassWriter(reader, 0);
    }
}

Wir verwenden dies als Basis, um die Cloneable -Schnittstelle zur stock Integer -Klasse hinzuzufügen, und fügen außerdem ein Feld und eine Methode hinzu.

4.1. Mit Feldern arbeiten

Erstellen wir unseren ClassVisitor , mit dem wir der Integer -Klasse ein Feld hinzufügen:

public class AddFieldAdapter extends ClassVisitor {
    private String fieldName;
    private String fieldDefault;
    private int access = org.objectweb.asm.Opcodes.ACC__PUBLIC;
    private boolean isFieldPresent;

    public AddFieldAdapter(
      String fieldName, int fieldAccess, ClassVisitor cv) {
        super(ASM4, cv);
        this.cv = cv;
        this.fieldName = fieldName;
        this.access = fieldAccess;
    }
}

Lassen Sie uns als Nächstes die Methode visitField überschreiben, in der wir zuerst prüfen, ob das Feld, das Sie hinzufügen möchten, bereits vorhanden ist, und ein Kennzeichen setzen, um den Status anzugeben.

Wir müssen den Methodenaufruf noch an die übergeordnete Klasse weiterleiten - dies muss geschehen, da die visitField -Methode für jedes Feld in der Klasse aufgerufen wird. Wenn der Anruf nicht weitergeleitet wird, werden keine Felder in die Klasse geschrieben. **

Diese Methode ermöglicht es uns auch, die Sichtbarkeit oder den Typ der vorhandenen Felder zu ändern :

@Override
public FieldVisitor visitField(
  int access, String name, String desc, String signature, Object value) {
    if (name.equals(fieldName)) {
        isFieldPresent = true;
    }
    return cv.visitField(access, name, desc, signature, value);
}

Wir prüfen zunächst das Flag, das in der früheren visitField -Methode festgelegt wurde, und rufen die visitField -Methode erneut auf. Diesmal geben wir den Namen, den Zugriffsmodifizierer und die Beschreibung an. Diese Methode gibt eine Instanz von FieldVisitor. zurück.

  • Die Methode visitEnd ist die letzte Methode, die in der Reihenfolge der Besuchermethoden aufgerufen wird. Dies ist die empfohlene Position, um die Feldeinfügungslogik auszuführen ** .

Dann müssen wir die visitEnd -Methode für dieses Objekt aufrufen, um zu signalisieren, dass wir dieses Feld bereits besucht haben: **

@Override
public void visitEnd() {
    if (!isFieldPresent) {
        FieldVisitor fv = cv.visitField(
          access, fieldName, fieldType, null, null);
        if (fv != null) {
            fv.visitEnd();
        }
    }
    cv.visitEnd();
}
  • Es ist wichtig sicherzustellen, dass alle verwendeten ASM-Komponenten aus dem Paket org.objectweb.asm stammen. ** Viele Bibliotheken verwenden die ASM-Bibliothek intern, und IDEs können die gebündelten ASM-Bibliotheken automatisch einfügen.

Wir verwenden jetzt unseren Adapter in der addField -Methode, erhalten eine transformierte Version von java.lang.Integer mit unserem hinzugefügten Feld:

public class CustomClassWriter {
    AddFieldAdapter addFieldAdapter;
   //...
    public byte[]addField() {
        addFieldAdapter = new AddFieldAdapter(
          "aNewBooleanField",
          org.objectweb.asm.Opcodes.ACC__PUBLIC,
          writer);
        reader.accept(addFieldAdapter, 0);
        return writer.toByteArray();
    }
}

Wir haben die Methoden visitField und visitEnd überschrieben.

Alles was mit Feldern zu tun ist, geschieht mit der Methode visitField . Das bedeutet, dass wir auch vorhandene Felder ändern können (z. B. ein privates Feld in ein öffentliches Feld umwandeln), indem Sie die gewünschten Werte ändern, die an die visitField -Methode übergeben werden.

4.2. Mit Methoden arbeiten

Das Generieren ganzer Methoden in der ASM-API ist umfangreicher als andere Operationen in der Klasse. Dies beinhaltet ein erhebliches Maß an Byte-Code-Manipulation auf niedriger Ebene und ist daher nicht Gegenstand dieses Artikels.

Für die meisten praktischen Anwendungen können wir jedoch entweder eine vorhandene Methode ändern, um sie zugänglicher zu machen (vielleicht öffentlich zu machen, damit sie überschrieben oder überladen werden kann) oder eine Klasse modifizieren, um sie erweiterbar zu machen .

Machen wir die toUnsignedString-Methode öffentlich:

public class PublicizeMethodAdapter extends ClassVisitor {
    public PublicizeMethodAdapter(int api, ClassVisitor cv) {
        super(ASM4, cv);
        this.cv = cv;
    }
    public MethodVisitor visitMethod(
      int access,
      String name,
      String desc,
      String signature,
      String[]exceptions) {
        if (name.equals("toUnsignedString0")) {
            return cv.visitMethod(
              ACC__PUBLIC + ACC__STATIC,
              name,
              desc,
              signature,
              exceptions);
        }
        return cv.visitMethod(
          access, name, desc, signature, exceptions);
   }
}

Wie bei der Feldänderung fangen wir lediglich die Besuchsmethode ab und ändern die gewünschten Parameter .

In diesem Fall verwenden wir die Zugriffsmodifizierer im Paket org.objectweb.asm.Opcodes , um die Sichtbarkeit der Methode zu ändern . Wir stecken dann unseren ClassVisitor ein:

public byte[]publicizeMethod() {
    pubMethAdapter = new PublicizeMethodAdapter(writer);
    reader.accept(pubMethAdapter, 0);
    return writer.toByteArray();
}

4.3. Mit Klassen arbeiten

Ähnlich wie beim Ändern von Methoden modifizieren wir Klassen, indem wir die entsprechende Besuchermethode abfangen . In diesem Fall fangen wir visit ab, die erste Methode in der Besucherhierarchie:

public class AddInterfaceAdapter extends ClassVisitor {

    public AddInterfaceAdapter(ClassVisitor cv) {
        super(ASM4, cv);
    }

    @Override
    public void visit(
      int version,
      int access,
      String name,
      String signature,
      String superName, String[]interfaces) {
        String[]holding = new String[interfaces.length + 1];
        holding[holding.length - 1]= cloneableInterface;
        System.arraycopy(interfaces, 0, holding, 0, interfaces.length);
        cv.visit(V1__8, access, name, signature, superName, holding);
    }
}

Wir überschreiben die visit -Methode, um das Cloneable -Interface zum Array von Interfaces hinzuzufügen, das von der Integer -Klasse unterstützt wird. Wir schließen dies wie alle anderen Verwendungszwecke unserer Adapter an.

5. Verwenden der geänderten Klasse

Wir haben also die Integer -Klasse geändert. Jetzt müssen wir die modifizierte Version der Klasse laden und verwenden können.

Zusätzlich zum einfachen Schreiben der Ausgabe von writer.toByteArray als Klassendatei auf die Festplatte gibt es einige andere Möglichkeiten, mit unserer angepassten Integer -Klasse zu interagieren.

5.1. Verwenden des TraceClassVisitor

Die ASM-Bibliothek stellt die Dienstklasse TraceClassVisitor zur Verfügung, die wir verwenden werden, um die geänderte Klasse zu betrachten So können wir bestätigen, dass unsere Änderungen geschehen sind ** .

Da der TraceClassVisitor ein ClassVisitor ist, können wir ihn als Ersatz für einen Standard ClassVisitor verwenden:

PrintWriter pw = new PrintWriter(System.out);

public PublicizeMethodAdapter(ClassVisitor cv) {
    super(ASM4, cv);
    this.cv = cv;
    tracer = new TraceClassVisitor(cv,pw);
}

public MethodVisitor visitMethod(
  int access,
  String name,
  String desc,
  String signature,
  String[]exceptions) {
    if (name.equals("toUnsignedString0")) {
        System.out.println("Visiting unsigned method");
        return tracer.visitMethod(
          ACC__PUBLIC + ACC__STATIC, name, desc, signature, exceptions);
    }
    return tracer.visitMethod(
      access, name, desc, signature, exceptions);
}

public void visitEnd(){
    tracer.visitEnd();
    System.out.println(tracer.p.getText());
}

Hier haben wir den ClassVisitor , den wir an unseren früheren PublicizeMethodAdapter übergeben haben, mit dem TraceClassVisitor angepasst.

Alle Besuche werden jetzt mit unserem Tracer durchgeführt, der dann den Inhalt der transformierten Klasse ausdrucken kann und alle Änderungen zeigt, die wir daran vorgenommen haben.

In der ASM-Dokumentation wird zwar angegeben, dass der TraceClassVisitor auf dem PrintWriter , der dem Konstruktor bereitgestellt wird, drucken kann, dies scheint jedoch in der neuesten Version von ASM nicht ordnungsgemäß zu funktionieren.

Glücklicherweise haben wir Zugriff auf den zugrunde liegenden Drucker in der Klasse und konnten den Textinhalt des Tracers in unserer überschriebenen visitEnd -Methode manuell ausdrucken.

5.2. Java-Instrumentierung verwenden

Dies ist eine elegantere Lösung, die es uns ermöglicht, mit der JVM über Instrumentation auf engerer Ebene zu arbeiten .

Um die java.lang.Integer -Klasse zu instrumentieren, schreiben wir einen Agenten, der als Befehlszeilenparameter mit der JVM konfiguriert wird. Der Agent benötigt zwei Komponenten:

  • Eine Klasse, die eine Methode mit dem Namen premain implementiert.

  • Eine Implementierung von

ClassFileTransformer , in dem wir die modifizierte Version unserer Klasse bedingt angeben

public class Premain {
    public static void premain(String agentArgs, Instrumentation inst) {
        inst.addTransformer(new ClassFileTransformer() {
            @Override
            public byte[]transform(
              ClassLoader l,
              String name,
              Class c,
              ProtectionDomain d,
              byte[]b)
              throws IllegalClassFormatException {
                if(name.equals("java/lang/Integer")) {
                    CustomClassWriter cr = new CustomClassWriter(b);
                    return cr.addField();
                }
                return b;
            }
        });
    }
}

Wir definieren nun unsere premain -Implementierungsklasse in einer JAR-Manifestdatei mithilfe des Javen-Plugins von Maven:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-jar-plugin</artifactId>
    <version>2.4</version>
    <configuration>
        <archive>
            <manifestEntries>
                <Premain-Class>
                    com.baeldung.examples.asm.instrumentation.Premain
                </Premain-Class>
                <Can-Retransform-Classes>
                    true
                </Can-Retransform-Classes>
            </manifestEntries>
        </archive>
    </configuration>
</plugin>

Durch das Erstellen und Verpacken unseres Codes wird das Glas erzeugt, das wir als Agenten laden können. Um unsere angepasste Integer -Klasse in einem hypothetischen „ YourClass.class “ zu verwenden:

java YourClass -javaagent:"/path/to/theAgentJar.jar"

6. Fazit

Während wir unsere Transformationen hier einzeln implementieren, können wir mit ASM mehrere Adapter miteinander verketten, um komplexe Transformationen von Klassen zu erreichen.

Zusätzlich zu den hier untersuchten grundlegenden Transformationen unterstützt ASM auch Interaktionen mit Annotationen, Generika und inneren Klassen.

Wir haben einiges von der Leistungsfähigkeit der ASM-Bibliothek gesehen - sie beseitigt viele Einschränkungen, die bei Bibliotheken von Drittanbietern und sogar bei Standard-JDK-Klassen auftreten können.

ASM wird häufig unter der Haube einiger der beliebtesten Bibliotheken (Spring, AspectJ, JDK usw.) verwendet, um schnell eine Menge "Magie" auszuführen.

Den Quellcode für diesen Artikel finden Sie im Projekt GitHub .