Eine Anleitung zur Manipulation von Java-Bytecode mit ASM

1. Einleitung

In diesem Artikel wird erläutert, wie Sie die ASM-Bibliothek zum Bearbeiten 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 :

 org.ow2.asm asm 6.0   org.ow2.asm asm-util 6.0  

Wir können die neuesten Versionen von asm und asm-util von Maven Central erhalten.

3. Grundlagen der ASM-API

Die ASM-API bietet zwei Arten der Interaktion mit Java-Klassen zur Transformation und Generierung: ereignisbasiert und baumbasiert.

3.1. Ereignisbasierte API

Diese API ist stark auf dem Basis Besuchermuster und ist ähnlich in Gefühl auf das SAX - Parsing - Modell von XML - Dokumente zu verarbeiten. Es besteht im Kern aus folgenden Komponenten:

  • ClassReader - Hilft beim Lesen von Klassendateien und ist der Beginn der Transformation einer Klasse
  • ClassVisitor - bietet die Methoden zum Transformieren der Klasse nach dem Lesen der unformatierten Klassendateien
  • ClassWriter - wird verwendet, um das Endprodukt der Klassentransformation auszugeben

Im ClassVisitor verfügen wir über alle Besuchermethoden, mit denen wir die verschiedenen Komponenten (Felder, Methoden usw.) einer bestimmten Java-Klasse berühren. Dazu stellen wir eine Unterklasse von ClassVisitor bereit , um Änderungen in einer bestimmten Klasse zu implementieren.

Aufgrund der Notwendigkeit, die Integrität der Ausgabeklasse in Bezug auf Java-Konventionen und den daraus resultierenden Bytecode beizubehalten, erfordert diese Klasse eine strikte Reihenfolge, in der ihre Methoden aufgerufen werden sollten, um die 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 entspricht dem JAXB-Modell für die Verarbeitung von XML-Dokumenten.

Es basiert immer noch auf der ereignisbasierten API, führt jedoch die ClassNode-Stammklasse ein. Diese Klasse dient als Einstiegspunkt in die Klassenstruktur.

4. Arbeiten mit der ereignisbasierten ASM-API

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

Wir müssen nur die erforderliche Besuchermethode überschreiben, um unsere Änderungen zu implementieren. Beginnen wir mit dem Einrichten der erforderlichen Komponenten:

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 Grundlage, um der Standard- Integer- Klasse die klonbare Schnittstelle hinzuzufügen , und wir fügen auch ein Feld und eine Methode hinzu.

4.1. Arbeiten mit Feldern

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

Als nächstes überschreiben wir die visitField- Methode , bei der wir zunächst prüfen, ob das Feld, das wir hinzufügen möchten , bereits vorhanden ist, und ein Flag setzen, um den Status anzuzeigen .

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.

Mit dieser Methode können wir auch die Sichtbarkeit oder den Typ vorhandener Felder ä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 überprüfen zuerst das in der früheren visitField- Methode gesetzte Flag und rufen die visitField- Methode erneut auf. Diesmal geben wir den Namen, den Zugriffsmodifikator und die Beschreibung an. Diese Methode gibt eine Instanz von FieldVisitor zurück.

Die visitEnd- Methode 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 nicht mehr besuchen:

@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 und 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 in Bezug auf Felder zu tun ist, geschieht mit der visitField- Methode. Dies bedeutet, dass wir auch vorhandene Felder ändern können (z. B. ein privates Feld in ein öffentliches umwandeln), indem wir die gewünschten Werte ändern, die an die visitField- Methode übergeben werden.

4.2. Arbeiten mit Methoden

Das Generieren ganzer Methoden in der ASM-API ist aufwändiger als andere Operationen in der Klasse. Dies ist mit einem erheblichen Aufwand an Bytecode-Manipulationen auf niedriger Ebene verbunden und geht daher über den Rahmen dieses Artikels hinaus.

Für die meisten praktischen Anwendungen können wir jedoch entweder eine vorhandene Methode ändern, um sie zugänglicher zu machen (möglicherweise öffentlich machen, damit sie überschrieben oder überladen werden kann), oder eine Klasse ändern, 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 this case, we use the access modifiers in the org.objectweb.asm.Opcodes package to change the visibility of the method. We then plug in our ClassVisitor:

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

4.3. Working With Classes

Along the same lines as modifying methods, we modify classes by intercepting the appropriate visitor method. In this case, we intercept visit, which is the very first method in the visitor hierarchy:

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

We override the visit method to add the Cloneable interface to the array of interfaces to be supported by the Integer class. We plug this in just like all the other uses of our adapters.

5. Using the Modified Class

So we've modified the Integer class. Now we need to be able to load and use the modified version of the class.

In addition to simply writing the output of writer.toByteArray to disk as a class file, there are some other ways to interact with our customized Integer class.

5.1. Using the TraceClassVisitor

The ASM library provides the TraceClassVisitor utility class that we'll use to introspect the modified class. Thus we can confirm that our changes have happened.

Because the TraceClassVisitor is a ClassVisitor, we can use it as a drop-in replacement for a standard ClassVisitor:

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

What we have done here is to adapt the ClassVisitor that we passed to our earlier PublicizeMethodAdapter with the TraceClassVisitor.

All the visiting will now be done with our tracer, which then can print out the content of the transformed class, showing any modifications we've made to it.

While the ASM documentation states that the TraceClassVisitor can print out to the PrintWriter that's supplied to the constructor, this doesn't appear to work properly in the latest version of ASM.

Fortunately, we have access to the underlying printer in the class and were able to manually print out the tracer's text contents in our overridden visitEnd method.

5.2. Using Java Instrumentation

This is a more elegant solution that allows us to work with the JVM at a closer level via Instrumentation.

To instrument the java.lang.Integer class, we write an agent that will be configured as a command line parameter with the JVM. The agent requires two components:

  • A class that implements a method named premain
  • An implementation of ClassFileTransformer in which we'll conditionally supply the modified version of our class
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; } }); } }

We now define our premain implementation class in a JAR manifest file using the Maven jar plugin:

 org.apache.maven.plugins maven-jar-plugin 2.4     com.baeldung.examples.asm.instrumentation.Premain   true     

Building and packaging our code so far produces the jar that we can load as an agent. To use our customized Integer class in a hypothetical “YourClass.class“:

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

6. Conclusion

While we implemented our transformations here individually, ASM allows us to chain multiple adapters together to achieve complex transformations of classes.

In addition to the basic transformations we examined here, ASM also supports interactions with annotations, generics, and inner classes.

We've seen some of the power of the ASM library — it removes a lot of limitations we might encounter with third-party libraries and even standard JDK classes.

ASM is widely used under the hood of some of the most popular libraries (Spring, AspectJ, JDK, etc.) to perform a lot of “magic” on the fly.

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