Jangan memaksa pendengar untuk berpikir

Pendahuluan



Selama proses pengembangan, sering kali perlu membuat instance kelas yang namanya disimpan dalam file konfigurasi XML, atau untuk memanggil metode yang namanya ditulis sebagai string sebagai nilai atribut anotasi. Dalam kasus seperti itu, jawabannya adalah satu: "Gunakan refleksi!".


Dalam versi baru dari CUBA Platform, salah satu tugas untuk meningkatkan kerangka kerja adalah untuk menyingkirkan penciptaan pengendali acara secara eksplisit di kelas pengontrol layar UI. Dalam versi sebelumnya, deklarasi handler dalam metode inisialisasi controller sangat berantakan dengan kode, jadi pada versi ketujuh kami memutuskan untuk membersihkan semuanya.


Seorang pendengar peristiwa hanyalah referensi ke metode yang perlu dipanggil pada waktu yang tepat (lihat templat Observer ). Template seperti itu cukup sederhana untuk diimplementasikan menggunakan kelas java.lang.reflect.Method . Pada startup, Anda hanya perlu memindai kelas, mengeluarkan metode yang dijelaskan dari mereka, menyimpan referensi ke mereka, dan menggunakan tautan untuk memanggil metode (atau metode) ketika peristiwa terjadi, seperti yang dilakukan dalam sebagian besar kerangka kerja. Satu-satunya hal yang menghentikan kami adalah bahwa banyak acara secara tradisional dihasilkan di UI, dan saat menggunakan API refleksi, Anda harus membayar sejumlah harga dalam bentuk waktu pemanggilan metode. Karena itu, kami memutuskan untuk melihat bagaimana lagi Anda bisa membuat penangan acara tanpa menggunakan refleksi.


Kami sudah menerbitkan materi tentang MethodHandles dan LambdaMetafactory di habr , dan materi ini adalah semacam kelanjutan. Kami akan memeriksa pro dan kontra dari menggunakan API refleksi, serta alternatif - menghasilkan kode dengan kompilasi AOT dan LambdaMetafactory, dan bagaimana itu digunakan dalam kerangka kerja CUBA.


Refleksi: Lama. Bagus Andal


Dalam ilmu komputer, refleksi atau refleksi (holonim dari introspeksi, refleksi bahasa Inggris) berarti suatu proses di mana program dapat melacak dan memodifikasi struktur dan perilaku sendiri saat runtime. (c) Wikipedia.


Bagi kebanyakan pengembang Java, refleksi bukanlah hal yang baru. Tampak bagi saya bahwa tanpa mekanisme ini, Java tidak akan menjadi Java, yang sekarang menempati pangsa pasar yang besar dalam pengembangan perangkat lunak aplikasi. Bayangkan saja: proksi, metode yang mengikat ke acara melalui anotasi, injeksi ketergantungan, aspek, dan bahkan instantiating driver JDBC di versi pertama JDK! Refleksi di mana-mana, adalah landasan dari semua kerangka kerja modern.


Apakah ada masalah dengan Refleksi sebagaimana diterapkan pada tugas kita? Kami telah mengidentifikasi tiga:


Kecepatan - panggilan metode melalui API Refleksi lebih lambat daripada panggilan langsung. Di setiap versi baru JVM, pengembang terus-menerus mempercepat panggilan melalui refleksi, kompiler JIT mencoba untuk lebih mengoptimalkan kode, tetapi bagaimanapun, perbedaan dibandingkan dengan pemanggilan metode langsung terlihat.


Mengetik - jika Anda menggunakan java.lang.reflect.Method dalam kode, maka ini hanya referensi ke beberapa metode. Dan tidak ada tertulis berapa banyak parameter yang dilewati dan apa jenisnya. Panggilan dengan parameter yang salah akan menghasilkan kesalahan dalam runtime, dan tidak pada tahap kompilasi atau mengunduh aplikasi.


Transparansi - jika metode yang dipanggil melalui refleksi gagal, maka kita harus mengarungi beberapa invoke() sebelum kita sampai ke dasar penyebab sebenarnya dari kesalahan.


Tetapi jika kita melihat kode dari penangan event Spring atau JPA di Hibernate, maka java.lang.reflect.Method akan ada di dalam. Dan dalam waktu dekat, saya pikir ini tidak mungkin berubah. Kerangka kerja ini terlalu besar dan terlalu terikat dengan mereka, dan tampaknya kinerja penangan acara di sisi server sudah cukup untuk memikirkan apa yang dapat Anda ganti panggilan melalui refleksi dengan.


Dan opsi apa lagi yang ada?


Kompilasi AOT dan pembuatan kode - memberikan aplikasi kecepatan kembali!


Kandidat pertama yang menggantikan API refleksi adalah pembuatan kode. Sekarang kerangka kerja seperti Micronaut atau Quarkus sudah mulai muncul, yang mencoba menyelesaikan dua masalah: mengurangi kecepatan peluncuran aplikasi dan mengurangi konsumsi memori. Kedua metrik ini sangat penting di zaman kita tentang wadah, layanan microser, dan arsitektur tanpa server, dan kerangka kerja baru mencoba menyelesaikannya dengan kompilasi AOT. Menggunakan teknik yang berbeda (Anda dapat membaca di sini , misalnya), kode aplikasi dimodifikasi sedemikian rupa sehingga semua panggilan refleksif ke metode, konstruktor, dll. diganti dengan panggilan langsung. Dengan demikian, Anda tidak perlu memindai kelas dan membuat kacang pada saat startup aplikasi, dan JIT mengoptimalkan kode lebih efisien saat runtime, yang memberikan peningkatan signifikan dalam kinerja aplikasi yang dibangun pada kerangka kerja tersebut. Apakah pendekatan ini memiliki kelemahan? Jawab: tentu saja ada.


Pertama, Anda tidak menjalankan kode yang Anda tulis. Kode sumber berubah selama kompilasi, jadi jika ada yang salah, kadang-kadang sulit untuk memahami di mana kesalahannya: dalam kode Anda atau dalam algoritme pembangkitan (biasanya dalam Anda, tentu saja ) Dan dari sini muncul masalah debugging - Anda harus men-debug kode Anda sendiri.


Yang kedua - untuk menjalankan aplikasi yang ditulis dalam kerangka kerja dengan kompilasi AOT, Anda memerlukan alat khusus. Anda tidak bisa mendapatkan dan menjalankan aplikasi yang ditulis dalam Quarkus, misalnya. Kami membutuhkan plugin khusus untuk maven / gradle, yang akan memproses kode Anda lebih dulu. Dan sekarang, jika ada kesalahan dalam kerangka kerja, Anda perlu memperbarui tidak hanya perpustakaan, tetapi juga plugin.


Sebenarnya, pembuatan kode juga bukan hal baru di dunia Java, tidak muncul dengan Micronaut atau Quarkus . Dalam satu bentuk atau lainnya, beberapa kerangka kerja menggunakannya. Di sini kita dapat memanggil lombok, aspekj dengan generasi kode pendahuluan untuk aspek atau eclipselink, yang menambahkan kode ke kelas entitas untuk deserialisasi yang lebih efisien. Di CUBA, kami menggunakan pembuatan kode untuk menghasilkan acara tentang perubahan dalam status entitas dan untuk memasukkan pesan validator dalam kode kelas untuk menyederhanakan bekerja dengan entitas di UI.


Untuk pengembang CUBA, menerapkan pembuatan kode statis untuk penangan acara akan menjadi langkah ekstrem karena banyak perubahan harus dilakukan dalam arsitektur internal dan di plugin untuk pembuatan kode. Apakah ada sesuatu yang tampak seperti refleksi tetapi lebih cepat?


LambdaMetafactory - panggilan metode yang sama, tetapi lebih cepat


Java 7 memperkenalkan instruksi baru untuk JVM - invokedynamic . Tentang dia ada laporan bagus oleh Vladimir Ivanov di jug.ru di sini . Awalnya dirancang untuk digunakan dalam bahasa dinamis seperti Groovy, instruksi ini adalah kandidat yang bagus untuk menerapkan metode di Jawa tanpa menggunakan refleksi. Pada saat yang sama dengan instruksi baru, API terkait muncul di JDK:


  • Class MethodHandle - muncul kembali di Java 7, tetapi masih belum terlalu sering digunakan
  • LambdaMetafactory - kelas ini sudah dari Java 8, ini menjadi pengembangan lebih lanjut dari API untuk panggilan dinamis, menggunakan MethodHandle di dalamnya.

Tampaknya MethodHandle , yang pada dasarnya berupa pointer yang diketik ke suatu metode (konstruktor, dll.), Akan dapat memenuhi peran java.lang.reflect.Method . Dan panggilan akan lebih cepat, karena semua jenis pemeriksaan yang dilakukan di Reflection API dengan setiap panggilan, dalam hal ini, dilakukan hanya sekali, ketika MethodHandle .


Namun sayang, MethodHandle murni ternyata lebih lambat dari panggilan melalui API refleksi. Keuntungan kinerja dapat dicapai dengan menjadikan MethodHandle statis, tetapi tidak dalam semua kasus. Ada diskusi yang sangat baik tentang kecepatan panggilan MethodHandle di milis OpenJDK .


Tetapi ketika kelas LambdaMetafactory , ada peluang nyata untuk mempercepat pemanggilan metode. LambdaMetafactory memungkinkan LambdaMetafactory untuk membuat objek lambda dan membungkus panggilan metode langsung di dalamnya, yang dapat diperoleh melalui MethodHandle . Dan kemudian, menggunakan objek yang dihasilkan, Anda dapat memanggil metode yang diinginkan. Berikut adalah contoh dari generasi yang membungkus metode pengambil diteruskan sebagai parameter ke BiFunction:


 private BiFunction createGetHandlerLambda(Object bean, Method method) throws Throwable { MethodHandles.Lookup caller = MethodHandles.lookup(); CallSite site = LambdaMetafactory.metafactory(caller, "apply", MethodType.methodType(BiFunction.class), MethodType.methodType(Object.class, Object.class, Object.class), caller.findVirtual(bean.getClass(), method.getName(), MethodType.methodType(method.getReturnType(), method.getParameterTypes()[0])), MethodType.methodType(method.getReturnType(), bean.getClass(), method.getParameterTypes()[0])); MethodHandle factory = site.getTarget(); BiFunction listenerMethod = (BiFunction) factory.invoke(); return listenerMethod; } 

Sebagai hasilnya, kami mendapatkan instance BiFunction alih-alih Metode. Dan sekarang, bahkan jika kita menggunakan Metode dalam kode kita, maka menggantinya dengan BiFunction tidaklah sulit. Ambil kode nyata (sedikit disederhanakan, benar) untuk memanggil method handler, yang ditandai @EventListener dari Spring Framework:


 public class ApplicationListenerMethodAdapter implements GenericApplicationListener { private final Method method; public void onApplicationEvent(ApplicationEvent event) { Object bean = getTargetBean(); Object result = this.method.invoke(bean, event); handleResult(result); } } 

Dan ini adalah kode yang sama, tetapi yang menggunakan pemanggilan metode melalui lambda:


 public class ApplicationListenerLambdaAdapter extends ApplicationListenerMethodAdapter { private final BiFunction funHandler; public void onApplicationEvent(ApplicationEvent event) { Object bean = getTargetBean(); Object result = funHandler.apply(bean, event); handleResult(result); } } 

Perubahan minimal, fungsinya sama, tetapi ada kelebihan:


Lambda memiliki tipe - itu ditentukan pada penciptaan, sehingga memanggil "hanya metode" akan gagal.


Jejak tumpukan lebih pendek - saat memanggil metode melalui lambda, hanya satu panggilan tambahan ditambahkan - apply() . Dan itu saja. Selanjutnya, metode itu sendiri disebut.


Tetapi kecepatan harus diukur.


Ukur kecepatannya


Untuk menguji hipotesis, kami membuat microbenchmark menggunakan JMH untuk membandingkan waktu eksekusi dan throughput saat memanggil metode yang sama dengan cara yang berbeda: melalui API refleksi, melalui LambdaMetafactory, dan juga menambahkan panggilan metode langsung untuk perbandingan. Tautan ke Metode dan lambda dibuat dan di-cache sebelum tes dimulai.


Parameter uji:


 @BenchmarkMode({Mode.Throughput, Mode.AverageTime}) @Warmup(iterations = 5, time = 1000, timeUnit = TimeUnit.MILLISECONDS) @Measurement(iterations = 10, time = 1000, timeUnit = TimeUnit.MILLISECONDS) 

Tes itu sendiri dapat diunduh dari GitHub dan dijalankan sendiri, jika tertarik.


Hasil tes untuk Oracle JDK 11.0.2 dan JMH 1.21 (jumlahnya mungkin bervariasi, tetapi perbedaannya tetap terlihat dan hampir sama):


Uji - Dapatkan NilaiThroughput (ops / us)Waktu Eksekusi (kami / op)
LambdaGetTest720,0118
ReflectionGetTest650,0177
DirectMethodGetTest2600,0048
Uji - Tetapkan NilaiThroughput (ops / us)Waktu Eksekusi (kami / op
LambdaSetTest960,0092
ReflectionSetTest580,0173
DirectMethodSetTest4150,0031

Rata-rata, ternyata memanggil metode melalui lambda sekitar 30% lebih cepat daripada melalui API refleksi. Ada diskusi besar lainnya tentang kinerja pemanggilan metode di sini jika ada yang tertarik dengan detailnya. Singkatnya - kenaikan kecepatan diperoleh, antara lain, karena fakta bahwa lambda yang dihasilkan dapat diuraikan dalam kode program, dan pemeriksaan tipe belum dilakukan, tidak seperti refleksi.


Tentu saja, tolok ukur ini cukup sederhana, tidak termasuk metode pemanggilan dalam hierarki kelas atau pengukuran kecepatan pemanggilan metode final. Tapi kami membuat pengukuran yang lebih kompleks, dan hasilnya selalu mendukung penggunaan LambdaMetafactory.


Gunakan


Dalam kerangka kerja CUBA versi 7, di pengontrol UI, Anda dapat menggunakan anotasi @Subscribe untuk "menandatangani" metode untuk peristiwa antarmuka pengguna tertentu. Secara internal, ini diterapkan pada LambdaMetafactory , tautan ke metode pendengar dibuat dan di-cache pada panggilan pertama.


Inovasi ini memungkinkan untuk sangat jelas kode, terutama dalam hal bentuk dengan sejumlah besar elemen, interaksi yang kompleks, dan, dengan demikian, dengan sejumlah besar penangan acara. Contoh sederhana dari CUBA QuickStart: Bayangkan Anda perlu menghitung ulang jumlah pesanan saat menambahkan atau menghapus item produk. Anda perlu menulis kode yang menjalankan metode calculateAmount() ketika koleksi berubah dalam entitas. Seperti apa tampilannya sebelumnya:


 public class OrderEdit extends AbstractEditor<Order> { @Inject private CollectionDatasource<OrderLine, UUID> linesDs; @Override public void init( Map<String, Object> params) { linesDs.addCollectionChangeListener(e -> calculateAmount()); } ... } 

Dan di CUBA 7, kode tersebut terlihat seperti ini:


 public class OrderEdit extends StandardEditor<Order> { @Subscribe(id = "linesDc", target = Target.DATA_CONTAINER) protected void onOrderLinesDcCollectionChange (CollectionChangeEvent<OrderLine> event) { calculateAmount(); } ... } 

Intinya: kodenya lebih bersih dan tidak ada metode magic init() , yang cenderung tumbuh dan diisi dengan event handler dengan meningkatnya kompleksitas formulir. Namun - kami bahkan tidak perlu membuat bidang dengan komponen tempat kami berlangganan, CUBA akan menemukan komponen ini dengan ID.


Kesimpulan


Meskipun munculnya kerangka kerja generasi baru dengan kompilasi AOT ( Micronaut , Quarkus ), yang memiliki keunggulan yang tak terbantahkan dibandingkan kerangka kerja "tradisional" (terutama, mereka dibandingkan dengan Spring ), masih ada sejumlah besar kode yang ditulis menggunakan API refleksi. (dan terima kasih untuk semua Musim Semi yang sama). Dan sepertinya Spring Framework saat ini masih menjadi pemimpin di antara kerangka pengembangan aplikasi dan kami akan bekerja dengan kode berbasis refleksi untuk waktu yang lama.


Dan jika Anda berpikir tentang menggunakan API Refleksi dalam kode Anda - apakah itu aplikasi atau kerangka kerja - pikirkan dua kali. Pertama, tentang pembuatan kode, dan kemudian tentang MethodHandles / LambdaMetafactory. Metode kedua dapat berubah menjadi lebih cepat, dan upaya pengembangan akan dihabiskan tidak lebih dari dalam hal menggunakan API Refleksi.


Beberapa tautan yang lebih bermanfaat:
Alternatif yang lebih cepat untuk Java Reflection
Peretasan Ekspresi Lambda di Jawa
Metode Menangani di Jawa
Java Reflection, tetapi jauh lebih cepat
Mengapa LambdaMetafactory 10% lebih lambat dari MethodHandle statis tetapi 80% lebih cepat dari MethodHandle non-statis?
Terlalu Cepat, Terlalu Megamorfik: apa yang memengaruhi kinerja pemanggilan metode di Jawa?

Source: https://habr.com/ru/post/id447022/


All Articles