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.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
- Java mencoba menemukan modul di folder
plugins
dan 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 implementasi BasicService
dalam modul core
. - 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
plugins1
dan plugins2
. - 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 implementasi BasicService
dalam modul core
. - 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.