Modul (JPMS)

Berkenalan dengan module-info dan alasan platform Java dimodularisasi.

Daftar isi

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.

text
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.

java Contoh: module-info.java milik modul penghitung.tarif
// tidak-dijalankan
module penghitung.tarif {
    exports penghitung.tarif;
}
java Contoh: kelas yang diekspor
// 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();
    }
}
java Contoh: package internal yang tidak diekspor
// 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.

java Contoh: module-info.java milik modul aplikasi.laporan
// tidak-dijalankan
module aplikasi.laporan {
    requires penghitung.tarif;
}
java Contoh: kelas utama yang memakai modul lain
// 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.

bash
# 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.LaporanUtama

Dengan 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.

DirektifArtinya
requires modulmodul ini membutuhkan modul tersebut pada waktu kompilasi dan berjalan
requires transitive modulketergantungan diwariskan ke modul yang membaca modul ini
exports packagepackage dapat dipakai seluruh modul lain
exports package to modulpackage hanya terbuka untuk modul yang disebut
opens packagepackage dapat diakses lewat reflection saat berjalan
uses layananmodul ini memakai layanan (service) yang dinyatakan interface-nya
provides layanan with kelasmodul 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.

modul penghitung.tarifpackage penghitung.tarifPenghitungTarifdiekspor lewat exportspackage penghitung.tarif.internalPerhitunganDasartidak diekspormodul aplikasi.laporanpackage aplikasi.laporanLaporanUtamarequiresditolak
Batas antar modul: package yang diekspor dapat diakses, package internal ditolak

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 exports pada modul pemiliknya.
  • Mengira deklarasi modul dapat dicoba lewat berkas tunggal seperti biasa. module-info.java hanya bermakna dalam proyek multi-berkas yang dikompilasi javac dan 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.
  • requires menyatakan ketergantungan, exports membuka package, dan keluarga direktifnya menyediakan gradasi keterbukaan hingga layanan lewat uses dan provides.
  • public berhenti berarti terbuka bagi siapa pun pada kode modular, sebagaimana terjadi pada JDK internal yang kini tertutup bagi aplikasi.
  • Proyek modular dikompilasi dengan javac dan dijalankan lewat module path, sedangkan jdeps memetakan ketergantungan dan jlink merakit runtime yang menyusut.

Referensi#