Panduan untuk Byte Buddy

1. Ikhtisar

Sederhananya, ByteBuddy adalah pustaka untuk menghasilkan kelas Java secara dinamis pada saat run-time.

Dalam artikel to-the-point ini, kita akan menggunakan framework untuk memanipulasi kelas yang ada, membuat kelas baru sesuai permintaan, dan bahkan mencegat pemanggilan metode.

2. Dependensi

Pertama, mari tambahkan ketergantungan ke proyek kita. Untuk proyek berbasis Maven, kita perlu menambahkan ketergantungan ini ke pom.xml kita :

 net.bytebuddy byte-buddy 1.7.1 

Untuk proyek berbasis Gradle, kita perlu menambahkan artefak yang sama ke file build.gradle kita :

compile net.bytebuddy:byte-buddy:1.7.1

Versi terbaru dapat ditemukan di Maven Central.

3. Membuat Kelas Java pada Runtime

Mari kita mulai dengan membuat kelas dinamis dengan mensubkelas kelas yang sudah ada. Kita akan melihat proyek Hello World klasik .

Dalam contoh ini, kami membuat tipe ( Kelas ) yang merupakan subkelas dari Object.class dan mengganti metode toString () :

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

Apa yang baru saja kami lakukan adalah membuat instance ByteBuddy. Kemudian, kami menggunakan subclass () API untuk memperluas Object.class , dan kami memilih toString () dari kelas super ( Object.class ) menggunakan ElementMatchers .

Akhirnya, dengan metode intercept () , kami menyediakan implementasi toString () dan mengembalikan nilai tetap.

Metode make () memicu pembuatan kelas baru.

Pada titik ini, kelas kita sudah dibuat tetapi belum dimuat ke JVM. Ini diwakili oleh instance DynamicType.Unloaded , yang merupakan bentuk biner dari jenis yang dihasilkan.

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

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

Sekarang, kita dapat membuat instance dynamicType dan menjalankan metode toString () di atasnya:

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

Perhatikan bahwa memanggil dynamicType.toString () tidak akan berfungsi karena itu hanya akan memanggil implementasi toString () dari ByteBuddy.class .

The newInstance () adalah metode refleksi Java yang membuat instance baru dari tipe yang diwakili oleh objek ByteBuddy ini ; dengan cara yang mirip dengan menggunakan kata kunci baru dengan konstruktor no-arg.

Sejauh ini, kita hanya bisa mengganti metode di kelas super tipe dinamis kita dan mengembalikan nilai tetap milik kita sendiri. Di bagian selanjutnya, kita akan melihat cara mendefinisikan metode kita dengan logika khusus.

4. Delegasi Metode dan Logika Kustom

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

Pada kenyataannya, aplikasi membutuhkan logika yang lebih kompleks daripada ini. Salah satu cara efektif untuk memfasilitasi dan menyediakan logika kustom ke jenis dinamis adalah dengan mendelegasikan panggilan metode.

Mari buat tipe dinamis yang membuat subkelas Foo.class yang memiliki metode sayHelloFoo () :

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

Selanjutnya, mari buat Bilah kelas lain dengan sayHelloBar () statis dari tanda tangan yang sama dan jenis kembalian sebagai sayHelloFoo () :

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

Sekarang, mari kita mendelegasikan semua pemanggilan sayHelloFoo () ke sayHelloBar () menggunakan DSL ByteBuddy . Ini memungkinkan kami untuk memberikan logika khusus, yang ditulis dalam Java murni, ke kelas yang baru kami buat saat 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());

Memanggil sayHelloFoo () akan memanggil sayHelloBar () yang sesuai.

Bagaimana ByteBuddy mengetahui metode mana di Bar.class yang akan dipanggil? Ini mengambil metode yang cocok sesuai dengan tanda tangan metode, jenis kembalian, nama metode, dan penjelasan.

Metode sayHelloFoo () dan sayHelloBar () tidak memiliki nama yang sama, tetapi memiliki tanda tangan metode dan tipe kembalian yang sama.

Jika ada lebih dari satu metode yang dapat dipanggil di Bar.class dengan tanda tangan yang cocok dan tipe kembalian, kita dapat menggunakan anotasi @BindingPriority untuk mengatasi ambiguitas.

@BindingPriority menggunakan argumen integer - semakin tinggi nilai integer, semakin tinggi prioritas pemanggilan implementasi tertentu. Jadi, sayHelloBar () akan lebih disukai daripada sayBar () dalam cuplikan kode di bawah ini:

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

5. Metode dan Definisi Bidang

Kami telah dapat mengganti metode yang dideklarasikan di kelas super tipe dinamis kami. Mari melangkah lebih jauh dengan menambahkan metode baru (dan bidang) ke kelas kita.

Kami akan menggunakan refleksi Java untuk memanggil metode yang dibuat secara dinamis:

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 subclass dari Object.class . Kami kemudian mendefinisikan metode, kustom, yang mengembalikan String dan memiliki pengubah akses publik .

Sama seperti yang kami lakukan di contoh sebelumnya, kami menerapkan metode kami dengan mencegat panggilan ke sana dan mendelegasikannya ke Bar.class yang kami buat sebelumnya di tutorial ini.

6. Mendefinisikan Ulang Kelas yang Ada

Meskipun kita telah bekerja dengan kelas yang dibuat secara dinamis, kita juga dapat bekerja dengan kelas yang sudah dimuat. Ini dapat dilakukan dengan mendefinisikan ulang (atau mengubah peringkat) kelas yang ada dan menggunakan ByteBuddyAgent untuk memuatnya kembali ke JVM.

Pertama, mari tambahkan ByteBuddyAgent ke pom.xml kita :

 net.bytebuddy byte-buddy-agent 1.7.1 

Versi terbaru dapat ditemukan di sini.

Sekarang, mari kita definisikan ulang metode sayHelloFoo () yang kita buat di Foo.class sebelumnya:

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. Kesimpulan

Dalam panduan yang rumit ini, kita telah melihat secara ekstensif kapabilitas pustaka ByteBuddy dan cara menggunakannya untuk pembuatan kelas dinamis yang efisien.

Dokumentasinya menawarkan penjelasan mendalam tentang cara kerja bagian dalam dan aspek lain dari perpustakaan.

Dan, seperti biasa, potongan kode lengkap untuk tutorial ini dapat ditemukan di Github.