Java 9 memecah platform dan aplikasi menjadi satuan yang disebut modul, lewat proyek besar bernama Project Jigsaw yang dituangkan dalam JEP 261. Sebuah modul adalah kelompok package beserta deklarasinya pada berkas module-info.java, yaitu daftar package yang dibagikan dan modul lain yang dibutuhkan. Topik ini menjelaskan masalah yang diselesaikan modularisasi, sintaks requires dan exports, serta akibat encapsulation kuat bagi kode yang memakai API internal. Karena deklarasi modul hanya bermakna dalam proyek multi-berkas yang dikompilasi dengan javac, seluruh potongan pada topik ini ditandai tidak-dijalankan dan dijelaskan lewat prosa serta diagram.
Masalah yang dijawab modularisasi#
Classpath yang dipakai sejak awal Java bersifat datar. Seluruh kelas dari seluruh JAR berbagi satu ruang nama, tanpa catatan tentang siapa membutuhkan siapa dan versi berapa. Ketergantungan yang hilang pun baru ketahuan saat program berjalan, biasanya berupa NoClassDefFoundError di tengah eksekusi, bukan saat program dimulai. Konflik versi antar JAR yang membawa kelas senama menjadi masalah klasik yang sering disebut jar hell.
JDK sendiri mengalami dua masalah serupa. Paket internal seperti keluarga sun.misc bersifat public dan dapat dipakai siapa pun, sehingga banyak aplikasi menyandarkan diri ke API yang tidak dijanjikan stabil, dan setiap upgrade JDK menjadi berisiko. Di sisi lain, seluruh kelas platform menumpuk dalam satu kesatuan besar, sehingga tidak ada cara bawaan untuk merakit runtime yang hanya memuat bagian yang dibutuhkan.
JPMS menjawabnya dengan dua jaminan. Konfigurasi yang andal berarti graf ketergantungan antarmodul diperiksa sejak program dimulai, sehingga modul yang hilang berhenti di awal, bukan di tengah jalan. Encapsulation kuat berarti hanya package yang diekspor yang dapat dipakai modul lain, sedangkan sisanya tertutup meskipun kelasnya public.
Deklarasi modul dengan module-info.java#
Bentuk proyek modular paling sederhana memuat dua modul, yaitu satu modul pustaka dan satu modul aplikasi yang memakainya. Strukturnya berbentuk pohon seperti berikut.
proyek-tarif/
├── src/
│ ├── penghitung.tarif/
│ │ ├── module-info.java
│ │ └── penghitung/
│ │ └── tarif/
│ │ ├── PenghitungTarif.java
│ │ └── internal/
│ │ └── PerhitunganDasar.java
│ └── aplikasi.laporan/
│ ├── module-info.java
│ └── aplikasi/
│ └── laporan/
│ └── LaporanUtama.java
└── mods/Modul pustaka mendeklarasikan dirinya dan mengekspor satu package, sedangkan package internal-nya sengaja tidak diekspor.
// tidak-dijalankan
module penghitung.tarif {
exports penghitung.tarif;
}// tidak-dijalankan
package penghitung.tarif;
import penghitung.tarif.internal.PerhitunganDasar;
public class PenghitungTarif {
public static double tarifPerKm(double km) {
return PerhitunganDasar.dasar() + km * PerhitunganDasar.perKm();
}
}// tidak-dijalankan
package penghitung.tarif.internal;
public class PerhitunganDasar {
static double dasar() {
return 5000.0;
}
static double perKm() {
return 2500.0;
}
}Modul aplikasi menyatakan ketergantungannya lewat requires, lalu kelas utamanya memakai kelas yang diekspor modul pustaka.
// tidak-dijalankan
module aplikasi.laporan {
requires penghitung.tarif;
}// tidak-dijalankan
package aplikasi.laporan;
import penghitung.tarif.PenghitungTarif;
public class LaporanUtama {
public static void main(String[] args) {
System.out.printf("Tarif 12 km: Rp %.0f%n", PenghitungTarif.tarifPerKm(12));
}
}Kompilasi dan eksekusinya berpindah dari classpath ke module path, dan kedua perintah berikut adalah bentuk yang lazim.
# tidak-dijalankan
# kompilasi seluruh modul ke folder mods
javac -d mods --module-source-path src $(find src -name "*.java")
# jalankan modul aplikasi beserta kelas utamanya
java --module-path mods --module aplikasi.laporan/aplikasi.laporan.LaporanUtamaDengan tarif dasar 5000 dan tarif per kilometer 2500, program di atas mencetak nilai 35000 untuk 12 kilometer. Yang lebih penting dari angkanya adalah pemeriksaan yang terjadi sebelum itu. java membaca graf modul aplikasi lebih dahulu, dan bila modul penghitung.tarif tidak ada di module path, program menolak berjalan sejak awal alih-alih gagal di tengah eksekusi.
requires, exports, dan keluarganya#
requires dan exports adalah dua direktif yang paling sering ditulis, tetapi deklarasi modul mengenal beberapa lagi yang masing-masing menjawab kebutuhan berbeda. Tabel berikut merangkum yang paling umum.
| Direktif | Artinya |
|---|---|
requires modul | modul ini membutuhkan modul tersebut pada waktu kompilasi dan berjalan |
requires transitive modul | ketergantungan diwariskan ke modul yang membaca modul ini |
exports package | package dapat dipakai seluruh modul lain |
exports package to modul | package hanya terbuka untuk modul yang disebut |
opens package | package dapat diakses lewat reflection saat berjalan |
uses layanan | modul ini memakai layanan (service) yang dinyatakan interface-nya |
provides layanan with kelas | modul ini menyediakan implementasi layanan tersebut |
Modul java.base yang memuat String, List, dan inti platform lain diimplisitkan pada setiap modul, sehingga tidak pernah ditulis dalam requires. Bentuk exports ... to dan opens menunjukkan gradasi yang tidak dimiliki classpath, yaitu keterbukaan yang diarahkan ke penerima tertentu. Pasangan uses serta provides menjadi dasar mekanisme layanan yang memisahkan interface dari implementasinya.
Encapsulation kuat dan akibatnya#
Pembatasan exports mengubah makna public pada kode bertingkat modul. Kelas PerhitunganDasar pada contoh tetap public, tetapi hanya modul penghitung.tarif yang dapat memakainya, karena package internal-nya tidak diekspor. Percobaan import penghitung.tarif.internal.PerhitunganDasar dari modul aplikasi berhenti di kompilasi dengan pesan bahwa package tersebut tidak terlihat, dan perbaikannya bukan mengubah akses modifiernya, melainkan mengevaluasi ulang package mana yang memang untuk konsumsi luar. Diagram berikut merangkum batas itu, yaitu satu package yang diekspor, satu package internal yang tertutup, dan satu arah requires antar modulnya.
Untuk platform, encapsulation kuat menutup package internal JDK yang selama ini dipakai bebas. Kasus yang terkenal adalah keluarga sun.misc.Base64 yang tidak dapat dipakai lagi, dan penggantinya memang telah disiapkan sejak Java 8 pada java.util.Base64 yang terletak di package standar. Alat jdeps membantu pemetaan sisa ketergantungan semacam itu, sedangkan jlink merakit runtime khusus yang hanya memuat modul yang diperlukan aplikasi, sehingga ukuran runtime turun dari kesatuan besar platform menjadi himpunan kecil yang dipilih sendiri.
Kesalahan umum#
- Lupa mengekspor package yang seharusnya dipakai modul lain. Kompilasi modul pemakai gagal karena package-nya tidak terlihat, dan perbaikannya menambahkan baris
exportspada modul pemiliknya. - Mengira deklarasi modul dapat dicoba lewat berkas tunggal seperti biasa.
module-info.javahanya bermakna dalam proyek multi-berkas yang dikompilasijavacdan dijalankan lewat module path, bukan lewat peluncur berkas tunggal. - Memakai nama modul yang bentrok. Nama modul bersifat unik di seluruh graf modul, sehingga dua pustaka dengan nama sama saling meniadakan, dan pilihlah nama yang stabil serta terstruktur.
- Menempatkan package yang sama di dua modul. Kondisi yang disebut split package ditolak JPMS, dan perbaikannya menggabungkan atau memisahkan isi package tersebut ke satu tempat yang jelas.
- Mengharapkan modul bernama dapat membaca kode yang diletakkan di classpath. Kode classpath membentuk modul tanpa nama yang membaca seluruh platform, tetapi arah baliknya tertutup, sehingga pustaka lama di classpath tidak terlihat oleh modul bernama.
Ringkasan#
- JPMS memperkenalkan modul dengan deklarasi
module-info.java, dengan dua jaminan utama berupa konfigurasi yang diperiksa sejak awal dan encapsulation kuat atas package yang tidak diekspor. requiresmenyatakan ketergantungan,exportsmembuka package, dan keluarga direktifnya menyediakan gradasi keterbukaan hingga layanan lewatusesdanprovides.publicberhenti berarti terbuka bagi siapa pun pada kode modular, sebagaimana terjadi pada JDK internal yang kini tertutup bagi aplikasi.- Proyek modular dikompilasi dengan
javacdan dijalankan lewat module path, sedangkanjdepsmemetakan ketergantungan danjlinkmerakit runtime yang menyusut.
Referensi#
- JEP 261: Module System — OpenJDK
— proposal yang menetapkan JPMS sejak Java 9, termasuk
module-infodan encapsulation kuat. - JEP 200: The Modular JDK — OpenJDK — rancangan pemecahan JDK itu sendiri menjadi modul-modulnya.
- Module Declarations, JLS SE 21 §7.7 — Oracle — spesifikasi sintaks deklarasi modul dan direktifnya.