Panduan untuk Byte Buddy

1. Gambaran keseluruhan

Ringkasnya, ByteBuddy adalah perpustakaan untuk menghasilkan kelas Java secara dinamik pada waktu berjalan.

Dalam artikel ini, kita akan menggunakan kerangka kerja untuk memanipulasi kelas yang ada, membuat kelas baru berdasarkan permintaan, dan bahkan memintas panggilan kaedah.

2. Kebergantungan

Mari kita tambah kebergantungan pada projek kita terlebih dahulu. Untuk projek berasaskan Maven, kita perlu menambahkan kebergantungan ini ke pom.xml kami :

 net.bytebuddy byte-buddy 1.7.1 

Untuk projek berasaskan Gradle, kita perlu menambahkan artifak yang sama ke fail build.gradle kami :

compile net.bytebuddy:byte-buddy:1.7.1

Versi terbaru boleh didapati di Maven Central.

3. Membuat Kelas Java di Runtime

Mari mulakan dengan membuat kelas yang dinamik dengan menundukkan kelas yang ada. Kita akan melihat projek Hello World klasik .

Dalam contoh ini, kami membuat tipe ( Class ) yang merupakan subkelas Object.class dan menimpa kaedah toString () :

DynamicType.Unloaded unloadedType = new ByteBuddy() .subclass(Object.class) .method(ElementMatchers.isToString()) .intercept(FixedValue.value("Hello World ByteBuddy!")) .make();

Yang kami buat hanyalah membuat contoh ByteBuddy. Kemudian, kami menggunakan subclass () API untuk memperluas Object.class , dan kami memilih toString () kelas super ( Object.class ) menggunakan ElementMatchers .

Akhirnya, dengan kaedah pintasan () , kami menyediakan pelaksanaan toString () dan mengembalikan nilai tetap.

Kaedah make () mencetuskan generasi kelas baru.

Pada ketika ini, kelas kami sudah dibuat tetapi belum dimuatkan ke JVM. Ia diwakili oleh contoh DynamicType.Unloaded , yang merupakan bentuk binari dari jenis yang dihasilkan.

Oleh itu, kita perlu memuatkan kelas yang dihasilkan ke dalam JVM sebelum kita dapat menggunakannya:

Class dynamicType = unloadedType.load(getClass() .getClassLoader()) .getLoaded();

Sekarang, kita dapat mewujudkan jenis DynamicType dan menggunakan kaedah toString () di atasnya:

assertEquals( dynamicType.newInstance().toString(), "Hello World ByteBuddy!");

Nota yang memanggil dynamicType.toString () tidak akan bekerja kerana itu hanya akan sembah yang toString () pelaksanaan ByteBuddy.class .

The NewInstance () adalah kaedah refleksi Java yang membuat contoh baru dari jenis yang diwakili oleh objek ByteBuddy ini ; dengan cara yang serupa dengan menggunakan kata kunci baru dengan konstruktor no-arg.

Setakat ini, kami hanya dapat mengatasi kaedah di kelas super jenis dinamik kami dan mengembalikan nilai tetap kami sendiri. Pada bahagian seterusnya, kita akan melihat cara menentukan kaedah kita dengan logik tersuai.

4. Pendelegasian Kaedah dan Logik Khusus

Dalam contoh sebelumnya, kami mengembalikan nilai tetap dari kaedah toString () .

Pada hakikatnya, aplikasi memerlukan logik yang lebih kompleks daripada ini. Salah satu kaedah yang berkesan untuk memfasilitasi dan menyediakan logik khusus kepada jenis dinamik adalah penyerahan panggilan kaedah.

Mari buat jenis dinamik yang subclass Foo.class yang mempunyai kaedah sayHelloFoo () :

public String sayHelloFoo() { return "Hello in Foo!"; }

Selanjutnya, mari buat Bar kelas lain dengan sayHelloBar statik () dengan tandatangan dan jenis pengembalian yang sama seperti sayHelloFoo () :

public static String sayHelloBar() { return "Holla in Bar!"; }

Sekarang, mari kita utarakan semua panggilan sayHelloFoo () untuk sayHelloBar () menggunakan DSL ByteBuddy . Ini membolehkan kami memberikan logik tersuai, ditulis dalam Java murni, kepada kelas kami yang baru dibuat pada waktu runtime:

String r = new ByteBuddy() .subclass(Foo.class) .method(named("sayHelloFoo") .and(isDeclaredBy(Foo.class) .and(returns(String.class)))) .intercept(MethodDelegation.to(Bar.class)) .make() .load(getClass().getClassLoader()) .getLoaded() .newInstance() .sayHelloFoo(); assertEquals(r, Bar.sayHelloBar());

Menyeru sayHelloFoo () akan memanggil sayHelloBar () sewajarnya.

Bagaimana ByteBuddy tahu kaedah mana yang boleh digunakan di Bar.class ? Ia memilih kaedah yang sesuai dengan kaedah tandatangan, jenis pengembalian, nama kaedah, dan anotasi.

Kaedah sayHelloFoo () dan sayHelloBar () tidak mempunyai nama yang sama, tetapi mereka mempunyai tandatangan kaedah dan jenis pengembalian yang sama.

Sekiranya terdapat lebih daripada satu kaedah yang tidak boleh dikelirukan dalam kelas Bar.class dengan tandatangan dan jenis pengembalian yang sepadan, kita boleh menggunakan anotasi @BindingPriority untuk menyelesaikan kesamaran.

@BindingPriority mengambil argumen integer - semakin tinggi nilai integer, semakin tinggi keutamaan memanggil pelaksanaan tertentu. Oleh itu, sayHelloBar () akan lebih disukai daripada sayBar () dalam coretan kod di bawah:

@BindingPriority(3) public static String sayHelloBar() { return "Holla in Bar!"; } @BindingPriority(2) public static String sayBar() { return "bar"; }

5. Kaedah dan Definisi Medan

Kami dapat mengatasi kaedah yang dinyatakan dalam kelas super jenis dinamik kami. Mari melangkah lebih jauh dengan menambahkan kaedah baru (dan medan) ke kelas kami.

Kami akan menggunakan pantulan Java untuk menggunakan kaedah yang dibuat secara dinamik:

Class type = new ByteBuddy() .subclass(Object.class) .name("MyClassName") .defineMethod("custom", String.class, Modifier.PUBLIC) .intercept(MethodDelegation.to(Bar.class)) .defineField("x", String.class, Modifier.PUBLIC) .make() .load( getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER) .getLoaded(); Method m = type.getDeclaredMethod("custom", null); assertEquals(m.invoke(type.newInstance()), Bar.sayHelloBar()); assertNotNull(type.getDeclaredField("x"));

Kami membuat kelas dengan nama MyClassName yang merupakan subkelas Object.class . Kami kemudian menentukan kaedah, kustom, yang mengembalikan String dan mempunyai pengubah akses awam .

Just like we did in previous examples, we implemented our method by intercepting calls to it and delegating them to Bar.class that we created earlier in this tutorial.

6. Redefining an Existing Class

Although we have been working with dynamically created classes, we can work with already loaded classes as well. This can be done by redefining (or rebasing) existing classes and using ByteBuddyAgent to reload them into the JVM.

First, let's add ByteBuddyAgent to our pom.xml:

 net.bytebuddy byte-buddy-agent 1.7.1 

The latest version can be found here.

Now, let's redefine the sayHelloFoo() method we created in Foo.class earlier:

ByteBuddyAgent.install(); new ByteBuddy() .redefine(Foo.class) .method(named("sayHelloFoo")) .intercept(FixedValue.value("Hello Foo Redefined")) .make() .load( Foo.class.getClassLoader(), ClassReloadingStrategy.fromInstalledAgent()); Foo f = new Foo(); assertEquals(f.sayHelloFoo(), "Hello Foo Redefined");

7. Conclusion

Dalam panduan yang terperinci ini, kami telah melihat secara mendalam kemampuan perpustakaan ByteBuddy dan cara menggunakannya untuk membuat kelas dinamik yang cekap.

Dokumentasinya menawarkan penjelasan mendalam mengenai cara kerja dalaman dan aspek lain dari perpustakaan.

Dan, seperti biasa, coretan kod lengkap untuk tutorial ini boleh didapati di Github.