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");  
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.coreplugin dapat mewarisi dari antarmukaIService(jika tidak,IServicetidak akan dapat diakses di luar modulcore).
- Kedua, ia mengumumkan bahwa ia menggunakan Layanan.
- Ketiga, ia mengatakan bahwa ia menyediakan implementasi layanan IServicemelalui kelasBasicService.
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
- Java mencoba menemukan modul di folder pluginsdan tidak menemukannya.
- Lapisan kosong telah dibuat.
- ServiceLoader mulai mencari semua implementasi IService.
- Di lapisan kosong, ia tidak menemukan implementasi layanan, karena tidak ada modul di sana.
- Setelah lapisan ini, ia terus mencari di lapisan induk (mis., Lapisan Boot) dan menemukan satu implementasiBasicServicedalam modulcore.
- 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:
- Java menemukan dua modul di folder plugins.
- Sebuah layer dibuat dengan dua modul plugins1danplugins2.
- ServiceLoader mulai mencari semua implementasi IService.
- Di lapisan plugin, ia menemukan dua implementasi layanan IService.
- Setelah itu, ia terus mencari di lapisan induk (mis., Lapisan Boot) dan menemukan satu implementasiBasicServicedalam modulcore.
- 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.javadan 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 usesdan menggunakanServiceLoader.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.