Plug-in Java tanpa rasa sakit

Pada artikel ini, saya ingin memberi tahu Anda cara membuat kerangka kerja aplikasi Java dengan cepat dan mudah dengan dukungan untuk pemuatan dinamis plugin. Pembaca mungkin akan segera berpikir bahwa tugas seperti itu telah dipecahkan untuk waktu yang lama, dan Anda dapat menggunakan kerangka kerja yang sudah jadi atau menulis loader kelas Anda, tetapi tidak ada yang akan diperlukan dalam solusi yang saya usulkan:

  • Kami tidak membutuhkan perpustakaan atau kerangka kerja khusus ( OSGi , Guice, dll.)
  • Kami tidak akan menggunakan parsing bytecode dengan ASM dan pustaka sejenis.
  • Kami tidak akan menulis pemuat kelas kami.
  • Kami tidak akan menggunakan refleksi dan anotasi.
  • Tidak perlu repot dengan classpath untuk menemukan plugin. Kami tidak akan menyentuh jalan setapak sama sekali.
  • Selain itu, kami tidak akan menggunakan XML, YAML atau bahasa deklaratif lainnya untuk mendeskripsikan titik ekstensi (titik ekstensi dalam plugin).

Namun, masih ada satu persyaratan - solusi semacam itu hanya akan bekerja di Java 9 atau lebih tinggi. Karena itu akan didasarkan pada modul dan layanan .

Jadi mari kita mulai. Kami merumuskan masalah lebih khusus:
Anda perlu menerapkan kerangka kerja aplikasi minimal, yang saat startup akan memuat plugin pengguna dari folder plugins .

Artinya, aplikasi yang dirakit akan terlihat seperti ini:

 plugin-app/ plugins/ plugin1.jar plugin2.jar ... core.jar โ€ฆ 

Mari kita mulai dengan modul core . Modul ini adalah inti dari aplikasi kita, yaitu, pada kenyataannya, adalah kerangka kerja kita.

Bagi mereka yang menghargai waktu, proyek yang sudah selesai tersedia di GitHub. Instruksi perakitan.
Tautan

 git clone https://github.com/orionll/plugin-app cd plugin-app mvn verify cd core/target java --module-path core-1.0-SNAPSHOT.jar --module core 

Buat 4 file Java berikut dalam modul:

 core/ src/main/java/ org/example/pluginapp/core/ IService.java BasicService.java Main.java module-info.java 

File pertama, IService.java adalah file yang menjelaskan titik ekstensi kami. Plugin lain kemudian akan dapat berkontribusi ke titik ekspansi ini ("berkontribusi"). Ini adalah prinsip standar untuk membangun aplikasi plug-in, yang disebut prinsip inversi ketergantungan (Dependency Inversion). Prinsip ini didasarkan pada kenyataan bahwa kernel tidak bergantung pada kelas-kelas tertentu, tetapi pada antarmuka.

Saya memberi titik ekstensi nama abstrak IService , karena saya sekarang menunjukkan konsep secara eksklusif. Pada kenyataannya, itu bisa berupa titik ekstensi tertentu, misalnya, jika Anda menulis editor grafis, itu bisa menjadi efek pemrosesan gambar, misalnya, IEffectProvider , IEffectContribution atau yang lainnya, tergantung pada bagaimana Anda lebih suka menyebutkan titik ekstensi. Pada saat yang sama, aplikasi itu sendiri akan berisi beberapa set efek dasar, dan pengembang pihak ketiga akan dapat menulis efek tambahan yang lebih canggih dan mengirimkannya dalam bentuk plug-in. Pengguna hanya perlu memasukkan efek-efek ini di folder plugins dan restart aplikasi.

File IService.java adalah sebagai berikut:

 โ€ฆ public interface IService { void doJob(); static List<IService> getServices(ModuleLayer layer) { return ServiceLoader .load(layer, IService.class) .stream() .map(Provider::get) .collect(Collectors.toList()); } } 

Jadi, IService hanyalah sebuah antarmuka yang melakukan beberapa doJob() abstrak (saya ulangi, detailnya tidak penting, pada kenyataannya itu akan menjadi sesuatu yang konkret).

Perhatikan juga metode getServices() kedua. Metode ini mengembalikan semua implementasi antarmuka IService yang ditemukan di lapisan modul ini dan orang tuanya. Kami akan membicarakan hal ini secara lebih rinci nanti.

File kedua, BasicService.java , adalah implementasi dasar dari antarmuka IService . Itu akan selalu ada, bahkan jika tidak ada plugin dalam aplikasi. Dengan kata lain, core bukan hanya inti, tetapi juga plugin untuk dirinya sendiri, yang akan selalu dimuat. File BasicService.java terlihat seperti ini:

 โ€ฆ public class BasicService implements IService { @Override public void doJob() { System.out.println("Basic service"); } } 

Untuk kesederhanaan, doJob() hanya mencetak string "Basic service" dan hanya itu.

Jadi, saat ini kami memiliki gambar berikut:



File ketiga, Main.java , adalah tempat metode main() diimplementasikan. Ada sedikit keajaiban di dalamnya, untuk memahami yang Anda perlu tahu apa itu layer modul.

Tentang lapisan modul


Ketika Java meluncurkan aplikasi, maka semua modul platform + modul yang tercantum dalam argumen --module-path (dan juga classpath , jika ada) jatuh ke dalam apa yang disebut layer Boot . Dalam kasus kami, jika kami mengkompilasi modul core.jar dan menjalankan java --module-path core.jar --module core dari baris perintah, maka setidaknya java.base dan modul core akan berada di lapisan Boot :



Lapisan Boot selalu ada di aplikasi Java apa pun, dan ini adalah konfigurasi sekecil mungkin. Sebagian besar aplikasi ada dalam satu lapisan modul. Namun, dalam kasus kami, kami ingin melakukan pemuatan dinamis plugin dari folder plugins . Kami hanya bisa memaksa pengguna untuk memperbaiki jalur peluncuran aplikasi sehingga ia sendiri menambahkan plugin yang diperlukan ke --module-path , tetapi ini tidak akan menjadi solusi terbaik. Terutama orang-orang yang bukan programmer dan tidak mengerti mengapa mereka harus naik ke suatu tempat dan memperbaiki sesuatu untuk hal yang begitu sederhana.

Untungnya, ada solusinya: Java memungkinkan Anda membuat layer modul Anda sendiri dalam runtime, yang akan memuat modul dari tempat yang kita butuhkan. Untuk keperluan kami, satu lapisan baru untuk plugin akan cukup, yang akan memiliki lapisan Boot sebagai induk (setiap lapisan harus memiliki induk):



Fakta bahwa lapisan plugin memiliki lapisan Boot sebagai induknya berarti bahwa modul-modul dari lapisan plugin dapat merujuk ke modul-modul dari lapisan Boot , tetapi tidak sebaliknya.

Jadi, mengetahui sekarang apa itu lapisan modul, Anda akhirnya dapat melihat isi file Main.java :

 โ€ฆ public final class Main { public static void main(String[] args) { Path pluginsDir = Paths.get("plugins"); //      plugins ModuleFinder pluginsFinder = ModuleFinder.of(pluginsDir); //  ModuleFinder      plugins       List<String> plugins = pluginsFinder .findAll() .stream() .map(ModuleReference::descriptor) .map(ModuleDescriptor::name) .collect(Collectors.toList()); //  ,      (   ) Configuration pluginsConfiguration = ModuleLayer .boot() .configuration() .resolve(pluginsFinder, ModuleFinder.of(), plugins); //      ModuleLayer layer = ModuleLayer .boot() .defineModulesWithOneLoader(pluginsConfiguration, ClassLoader.getSystemClassLoader()); //     IService       Boot List<IService> services = IService.getServices(layer); for (IService service : services) { service.doJob(); } } } 

Jika ini adalah pertama kalinya Anda melihat kode ini, ini mungkin terlihat sangat rumit, tetapi ini adalah sensasi yang salah karena banyaknya kelas baru yang tidak dikenal. Jika Anda mengerti sedikit tentang arti dari kelas ModuleFinder , Konfigurasi, dan ModuleLayer , maka semuanya jatuh ke tempatnya. Dan selain itu, hanya ada beberapa baris! Ini semua logika yang ditulis sekali.

Deskriptor modul


Ada satu lagi (keempat) file yang tidak kami pertimbangkan: module-info.java . Ini adalah file terpendek yang berisi deklarasi modul kami dan deskripsi layanan (titik ekstensi):

 โ€ฆ module core { exports org.example.pluginapp.core; uses IService; provides IService with BasicService; } 

Arti dari baris file ini harus jelas:

  • Pertama, modul mengekspor paket org.example.pluginapp.core plugin dapat mewarisi dari antarmuka IService (jika tidak, IService tidak akan dapat diakses di luar modul core ).
  • Kedua, ia mengumumkan bahwa ia menggunakan Layanan.
  • Ketiga, ia mengatakan bahwa ia menyediakan implementasi layanan IService melalui kelas BasicService .

Karena deklarasi modul ditulis dalam Java, kami mendapatkan keuntungan yang sangat penting: pemeriksaan kompiler dan jaminan statis . Misalnya, jika kami melakukan kesalahan atas nama tipe atau mengindikasikan paket yang tidak ada, kami akan segera mendapatkannya. Dalam kasus beberapa OSGi, kami tidak akan memiliki pemeriksaan pada waktu kompilasi, karena deklarasi titik ekstensi akan ditulis dalam XML.

Jadi, frame sudah siap. Mari kita coba jalankan:

 > java --module-path core.jar --module core Basic service 

Apa yang terjadi

  1. Java mencoba menemukan modul di folder plugins dan tidak menemukannya.
  2. Lapisan kosong telah dibuat.
  3. ServiceLoader mulai mencari semua implementasi IService .
  4. Di lapisan kosong, ia tidak menemukan implementasi layanan, karena tidak ada modul di sana.
  5. Setelah lapisan ini, ia terus mencari di lapisan induk (mis., Lapisan Boot ) dan menemukan satu implementasi BasicService dalam modul core .
  6. Semua implementasi yang ditemukan memiliki metode doJob() yang dipanggil. Karena hanya satu implementasi yang ditemukan, hanya "Basic service" yang dicetak.

Menulis sebuah plugin


Setelah menulis inti dari aplikasi kita, sekarang saatnya untuk mencoba menulis plugin untuk itu. Mari kita menulis dua plugin1 dan plugin2 : biarkan cetak pertama "Service 1" , yang kedua - "Service 2" . Untuk melakukan ini, Anda harus memberikan dua implementasi IService lagi di plugin1 dan plugin2 masing-masing:



Buat plugin pertama dengan dua file:

 plugin1/ src/main/java/ org/example/pluginapp/plugin1/ Service1.java module-info.java 

File Service1.java :

 โ€ฆ public class Service1 implements IService { @Override public void doJob() { System.out.println("Service 1"); } } 

File module-info.java :

 โ€ฆ module plugin1 { requires core; provides IService with Service1; } 

Perhatikan bahwa plugin1 bergantung pada core . Ini adalah prinsip inversi ketergantungan yang saya sebutkan sebelumnya: kernel tidak bergantung pada plugin, tetapi sebaliknya.

Plugin kedua sangat mirip dengan yang pertama, jadi saya tidak akan memberikannya di sini.

Sekarang mari kita kumpulkan plugin, letakkan di folder plugins dan jalankan aplikasi:

 > java --module-path core.jar --module core Service 1 Service 2 Basic service 

Hore, plugin sudah diambil! Bagaimana ini terjadi:

  1. Java menemukan dua modul di folder plugins .
  2. Sebuah layer dibuat dengan dua modul plugins1 dan plugins2 .
  3. ServiceLoader mulai mencari semua implementasi IService .
  4. Di lapisan plugin, ia menemukan dua implementasi layanan IService .
  5. Setelah itu, ia terus mencari di lapisan induk (mis., Lapisan Boot ) dan menemukan satu implementasi BasicService dalam modul core .
  6. Semua implementasi yang ditemukan memiliki metode doJob() yang dipanggil.

Perhatikan bahwa justru karena pencarian penyedia layanan dimulai dengan lapisan anak, dan kemudian pergi ke lapisan induk, kemudian "Service 1" dan "Service 2" dicetak terlebih dahulu, kemudian "Basic Service" . Jika Anda ingin layanan diurutkan sehingga layanan dasar berjalan lebih dulu, dan kemudian plugin, maka Anda dapat mengubah metode IService.getServices() dengan menambahkan pengurutan di sana (Anda mungkin perlu menambahkan metode int getOrdering() ke antarmuka IService ).

Ringkasan


Jadi, saya menunjukkan bagaimana Anda dapat dengan cepat dan efisien mengatur aplikasi Java plug-in yang memiliki properti berikut:

  • Kesederhanaan: untuk titik ekstensi dan pengikatannya, hanya fitur Java dasar (antarmuka, kelas, dan ServiceLoader ) yang digunakan, tanpa kerangka kerja, refleksi, anotasi, dan pemuat kelas.
  • Deklarabilitas: titik ekstensi dijelaskan dalam deskriptor modul. Lihat saja module-info.java dan pahami poin ekstensi apa yang ada dan plugin apa yang berkontribusi pada poin-poin ini.
  • Jaminan statis: jika terjadi kesalahan dalam deskriptor modul, program tidak akan dikompilasi. Juga, sebagai bonus, jika Anda menggunakan IntelliJ IDEA, Anda akan menerima peringatan tambahan (misalnya, jika Anda lupa menggunakan uses dan menggunakan ServiceLoader.load() )
  • Keamanan: sistem Java modular memeriksa saat startup bahwa konfigurasi modul sudah benar dan menolak untuk menjalankan program jika terjadi kesalahan.

Saya ulangi, saya hanya menunjukkan ide. Dalam aplikasi plug-in yang sebenarnya, akan ada puluhan hingga ratusan modul dan ratusan hingga ribuan titik ekstensi.

Saya memutuskan untuk mengangkat topik ini karena selama 7 tahun terakhir saya telah menulis aplikasi modular menggunakan Eclipse RCP, di mana OSGi terkenal digunakan sebagai sistem plug-in, dan deskriptor plug-in ditulis dalam XML. Kami memiliki lebih dari seratus plugin dan kami masih duduk di Java 8. Tetapi bahkan jika kami meningkatkan ke versi baru Java, kami tidak mungkin menggunakan modul Java, karena mereka sangat terikat dengan OSGi.

Tetapi jika Anda menulis aplikasi plug-in dari awal, maka modul Java adalah salah satu opsi yang mungkin untuk implementasinya. Ingat bahwa modul hanyalah alat, bukan tujuan.

Tentang saya secara singkat


Saya telah pemrograman selama lebih dari 10 tahun (8 di antaranya di Jawa), saya menanggapi StackOverflow dan menjalankan saluran saya sendiri di Telegram yang dikhususkan untuk Jawa.

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


All Articles