Panduan Manipulasi Bytecode Java dengan ASM

1. Pengenalan

Dalam artikel ini, kita akan melihat bagaimana menggunakan perpustakaan ASM untuk memanipulasi kelas Java yang ada dengan menambahkan bidang, menambahkan kaedah, dan mengubah tingkah laku kaedah yang ada.

2. Kebergantungan

Kita perlu menambahkan kebergantungan ASM ke pom.xml kami :

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

Kami boleh mendapatkan versi terbaru asm dan asm-util dari Maven Central.

3. Asas API ASM

API ASM menyediakan dua gaya berinteraksi dengan kelas Java untuk transformasi dan penjanaan: berasaskan acara dan berasaskan pokok.

3.1. API berasaskan acara

API ini sangat bergantung pada corak Pengunjung dan serupa dengan model penghuraian SAX yang memproses dokumen XML. Ia terdiri daripada inti dari komponen berikut:

  • ClassReader - membantu membaca fail kelas dan merupakan permulaan mengubah kelas
  • ClassVisitor - menyediakan kaedah yang digunakan untuk mengubah kelas setelah membaca fail kelas mentah
  • ClassWriter - digunakan untuk mengeluarkan produk akhir transformasi kelas

Di ClassVisitor , kita mempunyai semua kaedah pelawat yang akan kita gunakan untuk menyentuh komponen yang berbeza (bidang, kaedah, dll.) Dari kelas Java yang diberikan. Kami melakukan ini dengan menyediakan subkelas ClassVisitor untuk melaksanakan sebarang perubahan pada kelas tertentu.

Oleh kerana keperluan untuk menjaga integriti kelas output mengenai konvensi Java dan bytecode yang dihasilkan, kelas ini memerlukan susunan yang ketat di mana kaedahnya harus dipanggil untuk menghasilkan output yang betul.

The ClassVisitor kaedah API berasaskan peristiwa-the dipanggil untuk perkara berikut:

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

3.2. API berasaskan pokok

API ini adalah API yang lebih berorientasi objek dan serupa dengan model JAXB memproses dokumen XML.

Ia masih berdasarkan API berasaskan acara, tetapi memperkenalkan kelas root ClassNode . Kelas ini berfungsi sebagai pintu masuk ke dalam struktur kelas.

4. Bekerja Dengan API ASM berasaskan Acara

Kami akan mengubah kelas java.lang.Integer dengan ASM. Dan kita perlu memahami konsep asas pada ketika ini: yang ClassVisitor kelas mengandungi semua kaedah pengunjung perlu untuk mencipta atau mengubah suai semua bahagian-bahagian kelas .

Kami hanya perlu mengganti kaedah pelawat yang diperlukan untuk melaksanakan perubahan kami. Mari mulakan dengan menyediakan komponen prasyarat:

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

Kami menggunakan ini sebagai asas untuk menambahkan antara muka Cloneable ke kelas saham Integer , dan kami juga menambah medan dan kaedah.

4.1. Bekerja Dengan Ladang

Mari buat ClassVisitor kami yang akan kami gunakan untuk menambahkan medan ke kelas Integer :

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

Seterusnya, mari kita menimpa visitField kaedah , di mana kita mula-mula memeriksa jika medan kami merancang untuk menambah sudah wujud dan menetapkan bendera untuk menunjukkan status .

Kami masih perlu meneruskan panggilan kaedah ke kelas induk - ini perlu berlaku kerana kaedah visitField dipanggil untuk setiap bidang dalam kelas. Gagal meneruskan panggilan bermaksud tiada medan yang akan ditulis ke kelas.

Kaedah ini juga membolehkan kita mengubah penglihatan atau jenis bidang yang ada :

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

Kami mula-mula memeriksa bendera yang ditetapkan dalam kaedah visitField sebelumnya dan memanggil kaedah visitField sekali lagi, kali ini memberikan nama, pengubah akses, dan keterangan. Kaedah ini mengembalikan contoh FieldVisitor.

The visitEnd kaedah adalah kaedah terakhir yang dipanggil dalam susunan kaedah pelawat. Ini adalah kedudukan yang disyorkan untuk melaksanakan logik penyisipan medan .

Kemudian, kita perlu memanggil kaedah visitEnd pada objek ini untuk memberi isyarat bahawa kita sudah selesai melawat bidang ini:

@Override public void visitEnd() { if (!isFieldPresent) { FieldVisitor fv = cv.visitField( access, fieldName, fieldType, null, null); if (fv != null) { fv.visitEnd(); } } cv.visitEnd(); } 

Penting untuk memastikan bahawa semua komponen ASM yang digunakan berasal dari pakej org.objectweb.asm - banyak perpustakaan menggunakan perpustakaan ASM secara dalaman dan IDE dapat memasukkan perpustakaan ASM yang dikumpulkan secara automatik.

Kami sekarang menggunakan penyesuai kami dalam kaedah addField , memperoleh versi java.lang.Integer yang diubah dengan medan tambah kami:

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

Kami telah diatasi yang visitField dan visitEnd kaedah.

Segala sesuatu yang harus dilakukan mengenai bidang berlaku dengan kaedah visitField . Ini bererti kita juga dapat mengubahsuai bidang yang ada (katakanlah, mengubah bidang peribadi menjadi umum) dengan mengubah nilai yang diinginkan yang diteruskan ke kaedah visitField .

4.2. Bekerja Dengan Kaedah

Menjana keseluruhan kaedah dalam ASM API lebih banyak terlibat daripada operasi lain di kelas. Ini melibatkan sejumlah besar manipulasi kod byte tahap rendah dan, sebagai hasilnya, berada di luar ruang lingkup artikel ini.

Walau bagaimanapun, untuk kegunaan yang paling praktikal, kita boleh mengubah kaedah yang ada untuk menjadikannya lebih mudah diakses (mungkin menjadikannya umum sehingga dapat ditimpa atau dibebani) atau mengubah kelas untuk menjadikannya dapat diperluas .

Mari umumkan kaedah toUnsignedString:

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

Seperti yang kami lakukan untuk pengubahsuaian lapangan, kami hanya memintas kaedah lawatan dan mengubah parameter yang kami inginkan .

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.

Anda boleh mendapatkan kod sumber untuk artikel ini dalam projek GitHub.