JAR multi-rilis - Buruk atau Bagus?


Dari seorang penerjemah: kami secara aktif bekerja menerjemahkan platform ke rel Java 11 dan sedang berpikir tentang bagaimana mengembangkan perpustakaan Java secara efisien (seperti YARG ), dengan mempertimbangkan fitur-fitur Java 8/11 sehingga Anda tidak perlu membuat cabang dan rilis yang terpisah. Salah satu solusi yang mungkin adalah multi-release JAR, tetapi tidak semuanya lancar.


Java 9 termasuk opsi Java runtime baru yang disebut JAR multi-rilis. Ini mungkin salah satu inovasi paling kontroversial di platform. TL; DR: kami menemukan ini solusi bengkok untuk masalah serius . Dalam posting ini kami akan menjelaskan mengapa kami berpikir begitu, dan juga memberi tahu Anda bagaimana membangun JAR seperti itu jika Anda benar-benar menginginkannya.


JAR multi-rilis , atau MR JAR, adalah fitur baru platform Java, diperkenalkan di JDK 9. Di sini kami akan menjelaskan secara rinci risiko signifikan terkait dengan penggunaan teknologi ini, dan cara membuat JAR multi-rilis menggunakan Gradle, jika kamu masih mau.


Bahkan, JAR multi-rilis adalah arsip Java yang menyertakan beberapa varian dari kelas yang sama untuk bekerja dengan versi runtime yang berbeda. Misalnya, jika Anda bekerja di JDK 8, lingkungan Java akan menggunakan versi kelas untuk JDK 8, dan jika di Jawa 9, versi untuk Java 9 digunakan. Demikian pula, jika versi tersebut dibuat untuk rilis Java 10 di masa mendatang, runtime menggunakan versi ini sebagai ganti versi untuk Java 9 atau versi standar (Java 8).


Di bawah kucing, kami memahami perangkat format JAR baru dan mencari tahu apakah ini semua.


Kapan menggunakan JAR multi-rilis


  • Dioptimalkan runtime. Ini adalah solusi untuk masalah yang dihadapi banyak pengembang: ketika mengembangkan aplikasi, tidak diketahui di lingkungan mana ia akan dieksekusi. Namun, untuk beberapa versi runtime, Anda bisa menyematkan versi generik dari kelas yang sama. Misalkan Anda ingin menampilkan nomor versi Java di mana aplikasi sedang berjalan. Untuk Java 9, Anda dapat menggunakan metode Runtime.getVersion. Namun, ini adalah metode baru yang hanya tersedia di Java 9+. Jika Anda memerlukan runtime lain, katakanlah Java 8, Anda harus mengurai properti java.version. Akibatnya, Anda akan memiliki 2 implementasi yang berbeda dari satu fungsi.
  • API Konflik: Resolusi konflik antara API juga merupakan masalah umum. Misalnya, Anda perlu mendukung dua runtime, tetapi di salah satunya, API tidak digunakan lagi. Ada 2 solusi umum untuk masalah ini:


    1. Yang pertama adalah refleksi. Sebagai contoh, Anda dapat menentukan antarmuka VersionProvider, kemudian 2 kelas Java8VersionProvider dan Java9VersionProvider, dan memuat kelas terkait ke runtime (lucu bahwa untuk memilih antara dua kelas Anda harus menguraikan nomor versi!). Salah satu opsi untuk solusi ini adalah membuat kelas tunggal dengan berbagai metode yang disebut menggunakan refleksi.
    2. Solusi yang lebih maju adalah menggunakan Metode Menangani jika memungkinkan. Kemungkinan besar, refleksi bagi Anda akan terasa menghambat dan tidak nyaman, dan, secara umum, seperti apa adanya.


Alternatif yang dikenal untuk JAR multi-rilis


Cara kedua, lebih sederhana dan lebih mudah dipahami, adalah membuat 2 arsip berbeda untuk runtime yang berbeda. Secara teori, Anda membuat dua implementasi dari kelas yang sama di IDE, dan mengkompilasi, menguji dan mengemasnya dengan benar dalam 2 artefak berbeda adalah tugas sistem build. Ini adalah pendekatan yang telah digunakan di Jambu atau Spock selama bertahun-tahun. Tetapi juga diperlukan untuk bahasa seperti Scala. Dan semua karena ada begitu banyak opsi untuk kompiler dan runtime sehingga kompatibilitas biner menjadi hampir tidak mungkin tercapai.


Tetapi ada banyak alasan lain untuk menggunakan arsip JAR yang terpisah:


  • JAR hanyalah cara pengemasan.

itu adalah artefak rakitan yang mencakup kelas, tapi itu tidak semua: sumber daya, sebagai aturan, juga termasuk dalam arsip. Pengemasan, seperti penanganan sumber daya, memiliki harga. Tim Gradle bertujuan untuk meningkatkan kualitas pembuatan dan mengurangi waktu tunggu pengembang untuk menyusun hasil, pengujian, dan proses pembuatan secara umum. Jika arsip muncul dalam proses terlalu dini, titik sinkronisasi yang tidak perlu dibuat. Misalnya, untuk mengkompilasi kelas yang bergantung pada API, satu-satunya hal yang diperlukan adalah file .class. Arsip jar maupun sumber daya dalam jar tidak diperlukan. Demikian pula, hanya file Grade dan sumber daya yang diperlukan untuk menjalankan tes Gradle. Untuk pengujian, tidak perlu membuat toples. Itu hanya akan dibutuhkan oleh pengguna eksternal (mis., Ketika menerbitkan). Tetapi jika penciptaan artefak menjadi wajib, beberapa tugas tidak dapat dieksekusi secara paralel dan seluruh proses perakitan terhambat. Jika untuk proyek kecil ini tidak kritis, untuk proyek perusahaan skala besar ini adalah faktor perlambatan utama.


  • jauh lebih penting bahwa, sebagai artefak, arsip jar tidak dapat membawa informasi tentang ketergantungan.

Ketergantungan masing-masing kelas di Java 9 dan Java 8 runtime tidak mungkin sama. Ya, dalam contoh sederhana ini, tetapi untuk proyek yang lebih besar ini tidak benar: biasanya pengguna mengimpor backport perpustakaan untuk fungsionalitas Java 9 dan menggunakannya untuk mengimplementasikan versi kelas Java 8. Namun, jika Anda mengemas kedua versi ke dalam satu arsip, dalam satu artefak akan ada elemen dengan pohon ketergantungan berbeda. Ini berarti bahwa jika Anda bekerja dengan Java 9, Anda memiliki dependensi yang tidak akan pernah diperlukan. Selain itu, ini mencemari classpath, menciptakan kemungkinan konflik bagi pengguna perpustakaan.


Dan akhirnya, dalam satu proyek Anda dapat membuat JAR untuk tujuan yang berbeda:


  • untuk API
  • untuk java 8
  • untuk java 9
  • dengan penjilidan asli
  • dll.

Penggunaan classifier dependensi yang salah menyebabkan konflik terkait dengan pembagian mekanisme yang sama. Biasanya source atau javadocs diinstal sebagai classifier, tetapi pada kenyataannya mereka tidak memiliki dependensi.


  • Kami tidak ingin menghasilkan inkonsistensi, proses pembangunan seharusnya tidak bergantung pada bagaimana Anda mendapatkan kelas. Dengan kata lain, menggunakan stoples multi-rilis memiliki efek samping: panggilan dari arsip JAR dan panggilan dari direktori kelas sekarang merupakan hal yang sama sekali berbeda. Mereka memiliki perbedaan besar dalam semantik!
  • Bergantung pada alat apa yang Anda gunakan untuk membuat JAR, Anda mungkin berakhir dengan arsip JAR yang tidak kompatibel! Satu-satunya alat yang menjamin bahwa ketika Anda mengemas dua opsi kelas dalam satu arsip, mereka akan memiliki API terbuka tunggal - utilitas jar itu sendiri. Yang, bukan tanpa alasan, tidak harus melibatkan alat perakitan atau bahkan pengguna. JAR pada dasarnya adalah "amplop" yang menyerupai arsip ZIP. Jadi tergantung pada bagaimana Anda mengumpulkannya, Anda akan mendapatkan perilaku yang berbeda, atau mungkin Anda akan mengumpulkan artefak yang salah (dan Anda tidak akan menyadarinya).

Cara yang lebih efisien untuk mengelola arsip JAR individual


Alasan utama mengapa pengembang tidak menggunakan arsip terpisah adalah karena mereka tidak nyaman untuk mengumpulkan dan menggunakannya. Tool build yang harus disalahkan, yang, sebelum Gradle, tidak mengatasinya sama sekali. Secara khusus, mereka yang menggunakan metode ini di Maven hanya bisa mengandalkan fungsi classifier yang lemah untuk menerbitkan artefak tambahan. Namun, pengklasifikasi tidak membantu dalam situasi yang sulit ini. Mereka digunakan untuk berbagai keperluan, dari sumber penerbitan, dokumentasi, javadocs, untuk mengimplementasikan opsi perpustakaan (jambu-jdk5, jambu-jdk7, ...) atau berbagai kasus penggunaan (api, guci lemak, ...). Dalam praktiknya, tidak ada cara untuk menunjukkan bahwa pohon dependensi classifier berbeda dari pohon dependensi proyek utama. Dengan kata lain, format POM pada dasarnya rusak karena Ini mewakili bagaimana komponen dirakit dan artefak yang disuplai. Misalkan Anda perlu menerapkan 2 arsip jar yang berbeda: jar klasik dan gemuk, yang mencakup semua dependensi. Maven memutuskan bahwa 2 artefak memiliki pohon ketergantungan yang identik, bahkan jika ini jelas salah! Dalam hal ini, ini lebih dari jelas, tetapi situasinya sama dengan JAR multi-rilis!


Solusinya adalah menangani opsi dengan benar. Gradle dapat melakukan ini dengan mengelola dependensi berdasarkan opsi. Fitur ini saat ini tersedia untuk pengembangan di Android, tetapi kami juga sedang mengerjakan versinya untuk aplikasi Java dan asli!


Manajemen ketergantungan berbasis varian didasarkan pada kenyataan bahwa modul dan artefak adalah hal yang sangat berbeda. Kode yang sama dapat bekerja dengan sempurna di runtimes yang berbeda, dengan mempertimbangkan persyaratan yang berbeda. Bagi mereka yang bekerja dengan kompilasi asli, ini sudah jelas untuk waktu yang lama: kami mengkompilasi untuk i386 dan amd64 dan sama sekali tidak dapat kami mengganggu ketergantungan perpustakaan i386 dengan arm64 ! Dalam konteks Java, ini berarti bahwa untuk Java 8 Anda perlu membuat versi arsip JAR "java 8", di mana format kelas akan sesuai dengan Java 8. Artefak ini akan berisi metadata dengan informasi tentang dependensi mana yang akan digunakan. Untuk Java 8 atau 9, dependensi yang sesuai dengan versi akan dipilih. Sesederhana itu (sebenarnya, alasannya bukan karena runtime hanya satu bidang opsi, Anda dapat menggabungkan beberapa).


Tentu saja, tidak ada yang pernah melakukan ini sebelumnya karena kompleksitas yang berlebihan: Maven, tampaknya, tidak akan pernah membiarkan operasi yang rumit seperti itu dilakukan. Tetapi dengan Gradle itu mungkin. Tim Gradle saat ini sedang mengerjakan format metadata baru yang memberi tahu pengguna opsi mana yang akan digunakan. Sederhananya, alat bangunan harus menangani kompilasi, pengujian, pengemasan, dan pemrosesan modul tersebut. Sebagai contoh, proyek harus bekerja di Java 8 dan Java 9 runtime. Idealnya, Anda perlu mengimplementasikan 2 versi perpustakaan. Ini berarti bahwa ada 2 kompiler yang berbeda (untuk menghindari penggunaan Java 9 API ketika bekerja di Java 8), 2 direktori kelas dan, pada akhirnya, 2 arsip JAR yang berbeda. Dan juga, kemungkinan besar, perlu untuk menguji 2 runtime. Atau Anda menerapkan 2 arsip, tetapi memutuskan untuk menguji perilaku versi Java 8 di runtime Java 9 (karena ini dapat terjadi saat startup!).


Skema ini belum diterapkan, tetapi tim Gradle telah membuat kemajuan yang signifikan dalam arah ini.


Cara membuat JAR multi-rilis menggunakan Gradle


Tetapi jika fungsi ini belum siap, apa yang harus saya lakukan? Tenang, artefak yang benar diciptakan dengan cara yang sama. Sebelum fungsi di atas muncul di ekosistem Java, ada dua opsi:


  • cara lama yang baik menggunakan refleksi atau arsip JAR yang berbeda;
  • gunakan multi-release JARs (perhatikan bahwa ini bisa menjadi solusi yang buruk, bahkan jika ada kasus penggunaan yang baik).

Apa pun yang Anda pilih, arsip berbeda atau JAR multi-rilis, skemanya akan sama. JAR multi-rilis pada dasarnya adalah kemasan yang salah: mereka seharusnya menjadi pilihan, tetapi bukan tujuannya. Secara teknis, tata letak sumber sama untuk JAR individu dan eksternal. Repositori ini menjelaskan cara membuat JAR multi-rilis menggunakan Gradle. Inti dari proses ini dijelaskan secara singkat di bawah ini.


Pertama-tama, Anda harus selalu ingat satu kebiasaan buruk pengembang: mereka menjalankan Gradle (atau Maven) menggunakan versi Jawa yang sama di mana artefak direncanakan akan diluncurkan. Selain itu, terkadang versi yang lebih baru digunakan untuk meluncurkan Gradle, dan kompilasi terjadi dengan level API sebelumnya. Tetapi tidak ada alasan khusus untuk melakukannya. Di Gradle, kompilasi Ross dimungkinkan. Ini memungkinkan Anda untuk menggambarkan posisi JDK, serta menjalankan kompilasi sebagai proses terpisah, untuk mengkompilasi komponen menggunakan JDK ini. Cara terbaik untuk mengkonfigurasi berbagai JDK adalah dengan mengkonfigurasi path ke JDK melalui variabel lingkungan, seperti yang dilakukan dalam file ini . Maka Anda hanya perlu mengkonfigurasi Gradle untuk menggunakan JDK yang tepat, berdasarkan kompatibilitas dengan platform sumber / target . Perlu dicatat bahwa, mulai dengan JDK 9, versi JDK sebelumnya tidak diperlukan untuk kompilasi silang. Ini membuat fitur baru, -release. Gradle menggunakan fungsi ini dan akan mengonfigurasi kompiler sesuai kebutuhan.


Poin kunci lainnya adalah set sumber penunjukan. Set sumber adalah seperangkat file sumber yang perlu dikompilasi bersama. JAR diperoleh dengan mengkompilasi satu atau lebih set sumber. Untuk setiap set, Gradle secara otomatis membuat tugas kompilasi khusus yang sesuai. Ini berarti bahwa jika ada sumber untuk Java 8 dan Java 9, sumber-sumber ini akan berada dalam rangkaian yang berbeda. Ini persis cara kerjanya di set sumber untuk Java 9 , di mana akan ada versi kelas kami. Ini benar-benar berfungsi, dan Anda tidak perlu membuat proyek terpisah, seperti di Maven. Tetapi yang paling penting, metode ini memungkinkan Anda menyempurnakan kompilasi set.


Salah satu kesulitan memiliki versi yang berbeda dari satu kelas adalah bahwa kode kelas jarang terlepas dari sisa kode (itu memiliki ketergantungan dengan kelas yang tidak ada di set utama). Sebagai contoh, API-nya dapat menggunakan kelas-kelas yang tidak memerlukan sumber khusus untuk mendukung Java 9. Pada saat yang sama, saya tidak ingin mengkompilasi ulang semua kelas umum ini dan mengemas versinya untuk Java 9. Mereka adalah kelas-kelas umum, oleh karena itu mereka harus ada secara terpisah dari kelas untuk JDK tertentu. Kami atur di sini : tambahkan dependensi antara set sumber untuk Java 9 dan set utama, sehingga ketika mengkompilasi versi untuk Java 9, semua kelas umum tetap berada di kompilasi classpath.


Langkah selanjutnya adalah sederhana : Anda perlu menjelaskan kepada Gradle bahwa set sumber utama akan dikompilasi dengan level Java 8 API, dan set untuk Java 9 dengan level Java 9.


Semua hal di atas akan membantu Anda menggunakan kedua pendekatan yang disebutkan sebelumnya: menerapkan arsip JAR terpisah atau JAR multi-rilis. Karena postingan mengenai topik ini, mari kita lihat contoh bagaimana membuat Gradle membangun JAR multi-rilis:


jar { into('META-INF/versions/9') { from sourceSets.java9.output } manifest.attributes( 'Multi-Release': 'true' ) } 

Blok ini menjelaskan: mengemas kelas untuk Java 9 di direktori META-INF / versi / 9 , yang digunakan untuk MR JAR, dan mengatur label multi-rilis di manifes.


Dan itu saja, MR JAR pertama Anda sudah siap!


Tapi, sayangnya, pekerjaan ini belum berakhir. Jika Anda bekerja dengan Gradle, Anda tahu bahwa ketika Anda menggunakan plugin aplikasi, Anda dapat menjalankan aplikasi secara langsung melalui tugas yang dijalankan . Namun, karena fakta bahwa Gradle biasanya mencoba untuk meminimalkan jumlah pekerjaan, tugas yang dijalankan harus menggunakan direktori kelas dan direktori sumber daya yang diproses. Untuk JAR multi-rilis, ini merupakan masalah karena JAR diperlukan segera! Oleh karena itu, alih-alih menggunakan plugin, Anda harus membuat tugas Anda sendiri , dan ini adalah argumen yang menentang penggunaan multi-release JAR.


Dan yang tak kalah pentingnya, kami menyebutkan bahwa kami perlu menguji 2 versi kelas. Untuk ini, Anda hanya dapat menggunakan VM dalam proses terpisah, karena tidak ada padanan -release marker untuk Java runtime. Idenya adalah bahwa hanya satu tes yang perlu ditulis, tetapi akan dieksekusi dua kali: di Java 8 dan Java 9. Ini adalah satu-satunya cara untuk memastikan bahwa kelas runtime-spesifik bekerja dengan benar. Secara default, Gradle membuat satu tugas tes, dan menggunakan direktori kelas dengan cara yang sama, bukan JAR. Oleh karena itu, kami akan melakukan dua hal: membuat tugas tes untuk Java 9 dan mengkonfigurasi kedua tugas sehingga mereka menggunakan JAR dan runtime Java yang ditentukan. Implementasinya akan terlihat seperti ini:


 test { dependsOn jar def jdkHome = System.getenv("JAVA_8") classpath = files(jar.archivePath, classpath) - sourceSets.main.output executable = file("$jdkHome/bin/java") doFirst { println "$name runs test using JDK 8" } } task testJava9(type: Test) { dependsOn jar def jdkHome = System.getenv("JAVA_9") classpath = files(jar.archivePath, classpath) - sourceSets.main.output executable = file("$jdkHome/bin/java") doFirst { println classpath.asPath println "$name runs test using JDK 9" } } check.dependsOn(testJava9) 

Sekarang, ketika tugas dimulai, periksa Gradle akan mengkompilasi setiap set sumber menggunakan JDK yang diinginkan, buat JAR multi-rilis, kemudian jalankan tes menggunakan JAR ini di kedua JDK. Versi Gradle yang akan datang akan membantu Anda melakukan ini dengan lebih deklaratif.


Kesimpulan


Untuk meringkas. Anda mengetahui bahwa JAR multi-rilis merupakan upaya untuk memecahkan masalah nyata yang dihadapi banyak pengembang perpustakaan. Namun, solusi untuk masalah ini tampaknya tidak benar. Manajemen ketergantungan yang benar, menghubungkan artefak dan opsi, merawat kinerja (kemampuan untuk melaksanakan tugas sebanyak mungkin secara paralel) - semua ini menjadikan MR JAR solusi bagi masyarakat miskin. Masalah ini dapat diselesaikan dengan benar menggunakan opsi. Namun, sementara manajemen ketergantungan menggunakan opsi dari Gradle sedang dalam pengembangan, JAR multi-rilis cukup nyaman dalam kasus-kasus sederhana. Dalam hal ini, pos ini akan membantu Anda memahami bagaimana melakukan ini, dan bagaimana filosofi Gradle berbeda dari Maven (set sumber vs proyek).


Akhirnya, kami tidak menyangkal bahwa ada kasus-kasus di mana JAR multi-rilis masuk akal: misalnya, ketika tidak diketahui di lingkungan mana aplikasi akan dieksekusi (bukan perpustakaan), tetapi ini lebih merupakan pengecualian. Dalam posting ini, kami menggambarkan masalah utama yang dihadapi oleh pengembang perpustakaan, dan bagaimana JAR multi-rilis mencoba menyelesaikannya. Pemodelan dependensi yang benar karena opsi meningkatkan kinerja (melalui paralelisme berbutir halus) dan mengurangi biaya perawatan (menghindari kompleksitas yang tidak terduga) dibandingkan dengan JAR multi-rilis. Dalam situasi Anda, MR JAR mungkin juga dibutuhkan, jadi Gradle sudah menangani ini. Lihatlah contoh proyek ini dan cobalah sendiri.

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


All Articles