Pengalaman mentransfer proyek Maven ke Jar Multi-Rilis: sudah mungkin, tetapi masih sulit

Saya memiliki perpustakaan StreamEx kecil yang memperpanjang Java 8 Stream API. Secara tradisional saya mengumpulkan perpustakaan melalui Maven, dan sebagian besar saya senang dengan semuanya. Namun, saya ingin bereksperimen.


Beberapa hal di perpustakaan harus bekerja secara berbeda di berbagai versi Java. Contoh yang paling mencolok adalah metode Stream API baru seperti takeWhile , yang hanya muncul di Java 9. Pustaka saya juga menyediakan implementasi metode ini di Java 8, tetapi ketika Anda memperluas Stream API Anda sendiri, Anda memiliki beberapa batasan yang tidak akan saya sebutkan di sini. Saya berharap pengguna Java 9+ memiliki akses ke implementasi standar.


Agar proyek terus mengkompilasi menggunakan Java 8, ini biasanya dilakukan dengan menggunakan alat refleksi: kami mencari tahu apakah ada metode yang sesuai di perpustakaan standar dan, jika demikian, kami menyebutnya, dan jika tidak, kami menggunakan implementasi kami. Namun, saya memutuskan untuk menggunakan API MethodHandle , karena menyatakan lebih sedikit panggilan overhead. Anda bisa mendapatkan MethodHandle di muka dan menyimpannya di bidang statis:


 MethodHandles.Lookup lookup = MethodHandles.publicLookup(); MethodType type = MethodType.methodType(Stream.class, Predicate.class); MethodHandle method = null; try { method = lookup.findVirtual(Stream.class, "takeWhile", type); } catch (NoSuchMethodException | IllegalAccessException e) { // ignore } 

Dan kemudian menggunakannya:


 if (method != null) { return (Stream<T>)method.invokeExact(stream, predicate); } else { // Java 8 polyfill } 

Ini semua baik, tetapi terlihat jelek. Dan yang paling penting, di setiap titik di mana variasi implementasi dimungkinkan, Anda harus menulis kondisi seperti itu. Pendekatan yang sedikit alternatif adalah untuk memisahkan strategi Java 8 dan Java 9 sebagai implementasi dari antarmuka yang sama. Atau, untuk menyimpan ukuran perpustakaan, cukup terapkan semuanya untuk Java 8 dalam kelas non-final yang terpisah, dan gantikan pewaris untuk Java 9. Itu dilakukan seperti ini:


 //    Internals static final VersionSpecific VER_SPEC = System.getProperty("java.version", "").compareTo("1.9") > 0 ? new Java9Specific() : new VersionSpecific(); 

Kemudian pada titik-titik penggunaan, Anda cukup menulis return Internals.VER_SPEC.takeWhile(stream, predicate) . Semua keajaiban dengan pegangan metode sekarang hanya di kelas Java9Specific . Pendekatan ini, omong-omong, menyelamatkan perpustakaan untuk pengguna Android yang sebelumnya mengeluh bahwa itu tidak bekerja secara prinsip. Mesin virtual Android bukan Java, bahkan tidak mengimplementasikan spesifikasi Java 7. Secara khusus, tidak ada metode dengan tanda tangan polimorfik seperti invokeExact , dan keberadaan panggilan ini dalam bytecode akan merusak segalanya. Sekarang panggilan ini ditempatkan di kelas yang tidak pernah diinisialisasi.


Namun, semua ini masih jelek. Solusi yang indah (setidaknya secara teori) adalah dengan menggunakan Jar Rilis Multi, yang datang dengan Java 9 ( JEP-238 ). Untuk melakukan ini, bagian dari kelas harus dikompilasi di bawah Java 9 dan mengkompilasi file kelas yang ditempatkan di META-INF/versions/9 di dalam file Jar. Selain itu, Anda perlu menambahkan baris Multi-Release: true ke manifes. Kemudian Java 8 akan berhasil mengabaikan semua ini, dan Java 9 dan yang lebih baru akan memuat kelas baru, bukan kelas dengan nama yang sama yang terletak di tempat biasa.


Pertama kali saya mencoba melakukan ini lebih dari dua tahun yang lalu, tak lama sebelum rilis Java 9. Itu berjalan sangat sulit, dan saya berhenti. Bahkan sulit untuk membuat proyek dikompilasi oleh kompiler dari Java 9: ​​banyak plugin Maven rusak karena perubahan API internal, perubahan java.version string java.version atau yang lainnya.


Upaya baru tahun ini lebih berhasil. Sebagian besar plugin telah diperbarui dan berfungsi dengan cukup baik di Jawa baru. Langkah pertama saya menerjemahkan seluruh unit ke dalam Java 11. Untuk ini, selain memperbarui versi plugin, saya harus melakukan hal berikut:


  • Ubah tautan <a name="..."> dari formulir <a name="..."> menjadi <a id="..."> . Jika tidak, JavaDoc mengeluh.
  • Tentukan opsi additionalOptions = --no-module-directories di maven-javadoc-plugin. Tanpa ini, ada bug aneh dengan fitur pencarian JavaDoc: direktori dengan modul masih belum dibuat, tetapi ketika beralih ke hasil pencarian, /undefined/ ditambahkan ke path (halo, JavaScript). Fitur ini di Java 8 sama sekali tidak, jadi aktivitas saya sudah membawa hasil yang bagus: JavaDoc telah menjadi pencarian.
  • Perbaiki plugin untuk menerbitkan hasil cakupan pengujian di Coveralls ( coveralls-maven-plugin ). Untuk beberapa alasan itu ditinggalkan, yang aneh, mengingat bahwa Baju tinggal sendiri dan menawarkan layanan komersial. Jaxb-api yang digunakan plugin hilang dari Java 11. Untungnya, tidak sulit untuk memperbaiki masalah menggunakan alat Maven: cukup untuk secara eksplisit mendaftarkan ketergantungan pada plugin:
     <plugin> <groupId>org.eluder.coveralls</groupId> <artifactId>coveralls-maven-plugin</artifactId> <version>4.3.0</version> <dependencies> <dependency> <groupId>javax.xml.bind</groupId> <artifactId>jaxb-api</artifactId> <version>2.2.3</version> </dependency> </dependencies> </plugin> 

Langkah selanjutnya adalah adaptasi tes. Karena perilaku pustaka jelas berbeda di Java 8 dan Java 9, akan logis untuk menjalankan tes untuk kedua versi. Sekarang kita menjalankan semuanya di bawah Java 11, jadi kode khusus Java 8 tidak diuji. Ini adalah kode yang cukup besar dan non-sepele. Untuk mengatasinya, saya membuat pena buatan:


 static final VersionSpecific VER_SPEC = System.getProperty("java.version", "").compareTo("1.9") > 0 && !Boolean.getBoolean("one.util.streamex.emulateJava8") ? new Java9Specific() : new VersionSpecific(); 

Sekarang, lewati -Done.util.streamex.emulateJava8=true saat menjalankan tes,
untuk menguji apa yang biasanya berfungsi di Java 8. Sekarang tambahkan blok <execution> ke konfigurasi maven-surefire-plugin dengan argLine = -Done.util.streamex.emulateJava8=true , dan tes lulus dua kali.


Namun, saya ingin mempertimbangkan tes cakupan total. Saya menggunakan JaCoCo, dan jika Anda tidak memberi tahu apa-apa padanya, maka jalankan kedua hanya akan menimpa hasil yang pertama. Bagaimana cara kerja JaCoCo? Pertama-tama menjalankan target preparate prepare-agent , yang menetapkan properti argLine Maven, menandatangani sesuatu seperti -javaagent:blah-blah/.m2/org/jacoco/org.jacoco.agent/0.8.4/org.jacoco.agent-0.8.4-runtime.jar=destfile=blah-blah/myproject/target/jacoco.exec . Saya ingin dua file exec yang berbeda dibentuk. Anda dapat meretasnya dengan cara ini. Tambahkan destFile=${project.build.directory} konfigurasi destFile=${project.build.directory} prepare-agent . Kasar tapi efektif. Sekarang argLine akan berakhir dengan blah-blah/myproject/target . Ya, ini bukan file sama sekali, tetapi direktori. Tapi kita bisa mengganti nama file di awal tes. Kami kembali ke maven-surefire-plugin dan mengatur argLine = @{argLine}/jacoco_java8.exec -Done.util.streamex.emulateJava8=true untuk menjalankan Java 8 dan argLine = @{argLine}/jacoco_java11.exec untuk menjalankan Java 11. Maka mudah untuk menggabungkan dua file ini menggunakan target merge , yang disediakan oleh plugin JaCoCo, dan kami mendapatkan cakupan keseluruhan.


Perbarui: godin dalam komentar mengatakan bahwa itu tidak perlu, Anda dapat menulis ke satu file, dan itu akan secara otomatis merekatkan hasilnya dengan yang sebelumnya. Sekarang saya tidak yakin mengapa skenario ini pada awalnya tidak berhasil bagi saya.


Kami siap untuk beralih ke Jar Multi-Rilis. Saya menemukan sejumlah rekomendasi tentang cara melakukan ini. Yang pertama menyarankan penggunaan proyek multi-modular Maven. Saya tidak merasa seperti itu: ini adalah komplikasi besar dari struktur proyek: ada lima pom.xml, misalnya. Untuk bermain-main demi beberapa file yang perlu dikompilasi di Java 9 tampaknya berlebihan. Kompilasi awal yang disarankan lainnya melalui maven-antrun-plugin . Di sini saya memutuskan untuk hanya melihat sebagai pilihan terakhir. Jelas bahwa masalah apa pun di Maven dapat diselesaikan menggunakan Ant, tetapi ini agak canggung. Akhirnya, saya melihat rekomendasi untuk menggunakan plugin multi-release-jar-maven-plugin pihak ketiga. Itu sudah terdengar enak dan benar.


Plugin merekomendasikan penempatan kode sumber khusus untuk versi baru Java di direktori seperti src/main/java-mr/9 , yang saya lakukan. Saya masih memutuskan untuk menghindari tabrakan dalam nama kelas secara maksimal, sehingga satu-satunya kelas (bahkan antarmuka) yang ada di Java 8 dan Java 9, saya punya ini:


 // Java 8 package one.util.streamex; /* package */ interface VerSpec { VersionSpecific VER_SPEC = new VersionSpecific(); } // Java 9 package one.util.streamex; /* package */ interface VerSpec { VersionSpecific VER_SPEC = new Java9Specific(); } 

Konstanta lama pindah ke tempat baru, tetapi tidak ada yang benar-benar berubah. Hanya sekarang kelas Java9Specific menjadi lebih sederhana: semua squat dengan MethodHandle telah berhasil diganti dengan pemanggilan metode langsung.


Plugin berjanji untuk melakukan hal-hal berikut:


  • Ganti maven-compiler-plugin dan kompilasi dalam dua langkah dengan versi target yang berbeda.
  • Ganti maven-jar-plugin dan maven-jar-plugin hasil kompilasi dengan jalur yang benar.
  • Tambahkan baris Multi-Release: true ke MANIFEST.MF .

Agar bisa bekerja, dibutuhkan beberapa langkah.


  1. Ubah kemasan dari jar ke jar multi-release-jar .


  2. Tambahkan ekstensi-bangunan:


     <build> <extensions> <extension> <groupId>pw.krejci</groupId> <artifactId>multi-release-jar-maven-plugin</artifactId> <version>0.1.5</version> </extension> </extensions> </build> 

  3. Salin konfigurasi dari maven-compiler-plugin . Saya hanya memiliki versi default dengan semangat <source>1.8</source> dan <arg>-Xlint:all</arg>


  4. Saya berpikir bahwa maven-compiler-plugin sekarang dapat dihapus, tetapi ternyata plugin baru tidak menggantikan kompilasi tes, jadi untuk itu versi Java di-reset ke default (1.5!) Dan -Xlint:all argumen hilang. Jadi saya harus pergi.


  5. Agar tidak menduplikasi sumber dan target untuk dua plugin, saya menemukan bahwa mereka menghormati properti maven.compiler.source dan maven.compiler.target . Saya menginstalnya dan menghapus versi dari pengaturan plugin. Namun, tiba-tiba ternyata maven-javadoc-plugin menggunakan source dari pengaturan maven-compiler-plugin untuk mengetahui URL JavaDoc standar, yang harus ditautkan ketika merujuk metode standar. Dan sekarang dia tidak menghormati maven.compiler.source . Karenanya, saya harus mengembalikan <source>${maven.compiler.source}</source> ke pengaturan maven-compiler-plugin . Untungnya, tidak ada perubahan lain yang diperlukan untuk menghasilkan JavaDoc. Ini sangat baik dapat dihasilkan dari sumber Java 8, karena seluruh korsel dengan versi tidak mempengaruhi API perpustakaan.


  6. maven-bundle-plugin , yang mengubah perpustakaan saya menjadi artefak OSGi. Dia hanya menolak untuk bekerja dengan packaging = multi-release-jar . Pada prinsipnya, saya tidak pernah menyukainya. Dia menulis satu set baris tambahan ke manifes, pada saat yang sama merusak urutan penyortiran dan menambahkan lebih banyak sampah. Untungnya, ternyata tidak sulit untuk menghilangkannya dengan menulis semua yang Anda butuhkan dengan tangan. Hanya saja, tentu saja, bukan di maven-jar-plugin , tetapi di yang baru. Seluruh konfigurasi plugin multi-release-jar akhirnya menjadi seperti ini (saya mendefinisikan beberapa properti seperti project.package sendiri):


     <plugin> <groupId>pw.krejci</groupId> <artifactId>multi-release-jar-maven-plugin</artifactId> <version>0.1.5</version> <configuration> <compilerArgs><arg>-Xlint:all</arg></compilerArgs> <archive> <manifestEntries> <Automatic-Module-Name>${project.package}</Automatic-Module-Name> <Bundle-Name>${project.name}</Bundle-Name> <Bundle-Description>${project.description}</Bundle-Description> <Bundle-License>${license.url}</Bundle-License> <Bundle-ManifestVersion>2</Bundle-ManifestVersion> <Bundle-SymbolicName>${project.package}</Bundle-SymbolicName> <Bundle-Version>${project.version}</Bundle-Version> <Export-Package>${project.package};version="${project.version}"</Export-Package> </manifestEntries> </archive> </configuration> </plugin> 

  7. Tes. Kami tidak lagi memiliki one.util.streamex.emulateJava8 , tetapi Anda dapat mencapai efek yang sama dengan memodifikasi tes jalur kelas. Sekarang kebalikannya benar: secara default, perpustakaan berfungsi dalam mode Java 8, dan untuk Java 9 Anda perlu menulis:


     <classesDirectory>${basedir}/target/classes-9</classesDirectory> <additionalClasspathElements>${project.build.outputDirectory}</additionalClasspathElements> <argLine>@{argLine}/jacoco_java9.exec</argLine> 

    Poin penting: classes-9 harus mendahului file kelas biasa, jadi kami harus mentransfer yang biasa ke additionalClasspathElements , yang ditambahkan setelahnya.


  8. Sumber. Saya akan memiliki sumber-jar, dan akan lebih baik untuk mengemas sumber-sumber Java 9 ke dalamnya sehingga, misalnya, debugger di IDE dapat menampilkannya dengan benar. Saya tidak terlalu khawatir tentang duplikat VerSpec , karena ada satu baris, yang dieksekusi hanya selama inisialisasi. Tidak apa-apa bagi saya untuk hanya menyisakan versi dari Java 8. Namun, alangkah Java9Specific.java untuk melampirkan Java9Specific.java . Ini dapat dilakukan dengan menambahkan secara manual direktori sumber tambahan:


     <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>build-helper-maven-plugin</artifactId> <version>3.0.0</version> <executions> <execution> <phase>test</phase> <goals><goal>add-source</goal></goals> <configuration> <sou​rces> <sou​rce>src/main/java-mr/9</sou​rce> </sou​rces> </configuration> </execution> </executions> </plugin> 

    Setelah mengumpulkan artefak, saya menghubungkannya ke proyek uji dan memeriksanya di debugger IntelliJ IDEA. Semuanya berfungsi dengan baik: tergantung pada versi mesin virtual yang digunakan untuk menjalankan proyek pengujian, kami berakhir di sumber yang berbeda saat melakukan debugging.


    Akan sangat menyenangkan untuk melakukan ini dengan plugin multi-release-jar itu sendiri, jadi saya membuat saran seperti itu.


  9. JaCoCo. Ternyata itu yang paling sulit baginya, dan aku tidak bisa melakukannya tanpa bantuan dari luar. Faktanya adalah bahwa plug-in sempurna dihasilkan file-file untuk Java-8 dan Java-9, biasanya menempelkannya ke dalam satu file, namun, ketika menghasilkan laporan dalam XML dan HTML, mereka dengan keras kepala mengabaikan sumber-sumber dari Java-9. Mengaduk- aduk sumber , saya melihat bahwa itu hanya menghasilkan laporan untuk file kelas yang ditemukan di project.getBuild().getOutputDirectory() . Direktori ini, tentu saja, dapat diganti, tetapi sebenarnya saya memiliki dua di antaranya: classes dan classes-9 . Secara teoritis, Anda dapat menyalin semua kelas ke satu direktori, mengubah outputDirectory dan menjalankan JaCoCo, dan kemudian mengubah kembali outputDirectory agar tidak merusak perakitan JAR. Tapi itu terdengar sangat jelek. Secara umum, saya memutuskan untuk menunda solusi untuk masalah ini dalam proyek saya, tetapi saya menulis kepada orang - orang dari JaCoCo bahwa akan lebih baik untuk dapat menentukan beberapa direktori dengan file kelas.


    Yang mengejutkan saya, hanya beberapa jam kemudian salah satu pengembang JaCoCo godin datang ke proyek saya dan membawa permintaan tarik , yang menyelesaikan masalah. Bagaimana keputusannya? Menggunakan Semut, tentu saja! Ternyata Ant-plugin untuk JaCoCo lebih maju dan dapat menghasilkan laporan ringkasan untuk beberapa direktori sumber dan file kelas. Dia bahkan tidak memerlukan langkah merge terpisah, karena dia dapat memberi makan beberapa file sekaligus. Secara umum, Semut tidak bisa dihindari, jadi begitu. Hal utama yang berhasil, dan pom.xml hanya tumbuh enam baris.



    Saya bahkan tweet dalam hati:




Jadi saya punya proyek yang sangat bagus yang membangun Jar Multi-Rilis yang indah. Pada saat yang sama, persentase cakupan bahkan meningkat, karena saya menghapus semua jenis catch (NoSuchMethodException | IllegalAccessException e) yang tidak dapat dicapai di Jawa 9. Sayangnya, struktur proyek ini tidak didukung oleh IntelliJ IDEA, jadi saya harus meninggalkan impor POM dan mengkonfigurasi proyek dalam IDE secara manual . Saya berharap bahwa di masa depan masih akan ada solusi standar yang akan secara otomatis didukung oleh semua plugin dan alat.

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


All Articles