Panduan untuk Manipulasi Bytecode Java dengan ASM

1. Perkenalan

Pada artikel ini, kita akan melihat bagaimana menggunakan pustaka ASM untuk memanipulasi kelas Java yang ada dengan menambahkan bidang, menambahkan metode, dan mengubah perilaku metode yang ada.

2. Dependensi

Kita perlu menambahkan dependensi ASM ke pom.xml kita :

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

Kita bisa mendapatkan asm dan asm-util versi terbaru dari Maven Central.

3. Dasar-dasar ASM API

ASM API menyediakan dua gaya interaksi dengan kelas Java untuk transformasi dan pembuatan: berbasis peristiwa dan berbasis pohon.

3.1. API berbasis acara

API ini sangat didasarkan pada pola Pengunjung dan mirip dengan model penguraian SAX untuk pemrosesan dokumen XML. Ini terdiri, pada intinya, dari komponen-komponen berikut:

  • ClassReader - membantu membaca file kelas dan merupakan awal dari transformasi kelas
  • ClassVisitor - menyediakan metode yang digunakan untuk mengubah kelas setelah membaca file kelas mentah
  • ClassWriter - digunakan untuk mengeluarkan produk akhir dari transformasi kelas

Di ClassVisitor kita memiliki semua metode pengunjung yang akan kita gunakan untuk menyentuh komponen berbeda (bidang, metode, dll.) Dari kelas Java tertentu. Kami melakukan ini dengan menyediakan subclass ClassVisitor untuk mengimplementasikan perubahan apa pun di kelas tertentu.

Karena kebutuhan untuk menjaga integritas kelas keluaran terkait konvensi Java dan bytecode yang dihasilkan, kelas ini memerlukan urutan yang ketat di mana metodenya harus dipanggil untuk menghasilkan keluaran yang benar.

Metode ClassVisitor di API berbasis peristiwa dipanggil dalam urutan berikut:

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

3.2. API berbasis pohon

API ini adalah API yang lebih berorientasi objek dan analog dengan model pemrosesan dokumen XML JAXB .

Ini masih didasarkan pada API berbasis peristiwa, tetapi itu memperkenalkan kelas akar ClassNode . Kelas ini berfungsi sebagai pintu masuk ke dalam struktur kelas.

4. Bekerja Dengan API ASM berbasis Event

Kami akan memodifikasi kelas java.lang.Integer dengan ASM. Dan kita perlu memahami konsep mendasar pada titik ini: yang ClassVisitor kelas berisi semua metode pengunjung yang diperlukan untuk membuat atau mengubah semua bagian dari kelas .

Kami hanya perlu mengganti metode pengunjung yang diperlukan untuk menerapkan perubahan kami. Mari kita mulai dengan menyiapkan 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 dasar untuk menambahkan antarmuka Cloneable ke kelas Integer stok , dan kami juga menambahkan bidang dan metode.

4.1. Bekerja Dengan Bidang

Mari buat ClassVisitor yang akan kita gunakan untuk menambahkan bidang 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; } } 

Selanjutnya, mari kita timpa metode visitField , di mana pertama-tama kita memeriksa apakah bidang yang akan kita tambahkan sudah ada dan menyetel bendera untuk menunjukkan status .

Kita masih harus meneruskan panggilan metode ke kelas induk - ini perlu terjadi karena metode visitField dipanggil untuk setiap bidang di kelas. Gagal meneruskan panggilan berarti tidak ada bidang yang akan ditulis ke kelas.

Metode ini juga memungkinkan kami untuk mengubah visibilitas 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); } 

Pertama-tama kita memeriksa bendera yang disetel dalam metode visitField sebelumnya dan memanggil metode visitField lagi, kali ini memberikan nama, pengubah akses, dan deskripsi. Metode ini mengembalikan instance FieldVisitor.

The visitEnd metode adalah metode terakhir yang disebut dalam rangka metode pengunjung. Ini adalah posisi yang disarankan untuk menjalankan logika penyisipan bidang .

Kemudian, kita perlu memanggil metode visitEnd pada objek ini untuk memberi sinyal bahwa kita sudah selesai mengunjungi 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 bahwa semua komponen ASM yang digunakan berasal dari paket org.objectweb.asm - banyak pustaka menggunakan pustaka ASM secara internal dan IDE dapat memasukkan pustaka ASM yang dibundel secara otomatis.

Kami sekarang menggunakan adaptor kami dalam metode addField , mendapatkan versi java.lang.Integer yang diubah dengan kolom yang kami tambahkan:

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 mengganti metode visitField dan visitEnd .

Segala sesuatu yang harus dilakukan terkait bidang terjadi dengan metode visitField . Ini berarti kita juga dapat mengubah bidang yang ada (misalnya, mengubah bidang pribadi menjadi publik) dengan mengubah nilai yang diinginkan diteruskan ke metode visitField .

4.2. Bekerja Dengan Metode

Menghasilkan seluruh metode di ASM API lebih terlibat daripada operasi lain di kelas. Ini melibatkan sejumlah besar manipulasi kode byte tingkat rendah dan, akibatnya, berada di luar cakupan artikel ini.

Namun, untuk sebagian besar penggunaan praktis, kita dapat memodifikasi metode yang ada agar lebih dapat diakses (mungkin membuatnya menjadi publik sehingga dapat diganti atau dibebani) atau memodifikasi kelas agar dapat diperluas .

Mari jadikan metode toUnsignedString publik:

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 modifikasi bidang, kami hanya memotong metode kunjungan 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 dapat menemukan kode sumber untuk artikel ini di proyek GitHub.