Deskriptor module hanya berguna bila kita benar-benar menyusunnya. Dua direktif paling mendasar — requires dan exports — membentuk graf keterbacaan (readability graph): module A menyatakan membaca module B, module B menyatakan membuka package tertentu, dan compiler serta JVM masing-masing menegakkan sisi kontrak itu. Pada Chapter ini kita akan membangun dua module sungguhan dari nol, mengompilasinya dengan --module-source-path, membuktikan mekanisme keterbacaan lewat reflection ringan, lalu mengamati error presisi yang muncul ketika satu baris requires atau exports terlupakan.

Dua Module Pertama Kita #

Skenarionya: module com.pelanggan.api menyimpan class penyapa yang menjadi API, sedangkan module com.pelanggan.app adalah aplikasi yang memakainya. Keduanya kita taruh dalam satu pohon sumber bersama:

text Struktur dua module
src/
├── com.pelanggan.api/
│   ├── module-info.java
│   └── com/
│       └── pelanggan/
│           └── api/
│               └── PelangganApi.java
└── com.pelanggan.app/
    ├── module-info.java
    └── com/
        └── pelanggan/
            └── app/
                └── AplikasiUtama.java

Setiap module hidup dalam direktori bernama persis nama module-nya, dan di dalamnya berlaku kembali aturan package-direktori yang sudah kita kenal. Tiga berkas kuncinya pendek:

java src/com.pelanggan.api/module-info.java
module com.pelanggan.api {
    exports com.pelanggan.api;
}
java src/com.pelanggan.api/com/pelanggan/api/PelangganApi.java
package com.pelanggan.api;

public class PelangganApi {
    public static String sapa(String nama) {
        return "Halo, " + nama + "!";
    }
}
java src/com.pelanggan.app/module-info.java
module com.pelanggan.app {
    requires com.pelanggan.api;
}

Direktif exports com.pelanggan.api; membuka satu-satunya package module API, dan requires com.pelanggan.api; di sisi aplikasi menyatakan ketergantungan itu. Kompilasi multi-module dilakukan sekali jalan dengan --module-source-path:

bash Kompilasi dan eksekusi dua module
javac -d mods --module-source-path src \
    src/com.pelanggan.api/module-info.java \
    src/com.pelanggan.api/com/pelanggan/api/PelangganApi.java \
    src/com.pelanggan.app/module-info.java \
    src/com.pelanggan.app/com/pelanggan/app/AplikasiUtama.java
java --module-path mods --module com.pelanggan.app/com.pelanggan.app.AplikasiUtama

Output yang Dihasilkan:

text
Halo, Budi!

Opsi -d mods menghasilkan satu subdirektori per module di bawah mods — mods/com.pelanggan.api/ dan mods/com.pelanggan.app/, masing-masing berisi bytecode berikut module-info.class. Launcher lalu memakai --module-path mods alih-alih classpath, dan --module menyebut module titik masuk beserta nama class utamanya dengan format nama.module/nama.package.ClassUtama. Graf keterbacaannya sederhana:

Panah requires mengalir dari pembaca ke yang dibaca, java.base dibaca semua module tanpa perlu deklarasi, dan satu-satunya jalur akses data adalah package yang diekspor.

Mekanisme Keterbacaan #

Kata “membaca” bukan metafora — JVM menyimpan graf ini dan class Module punya method untuk menanyakannya. Program berikut memeriksa keterbacaan antar module JDK yang berjalan saat ini:

java KejarModul.java
public class KejarModul {
    public static void main(String[] args) {
        ModuleLayer lapisan = ModuleLayer.boot();
        Module sql = lapisan.findModule("java.sql").orElseThrow();
        Module xml = lapisan.findModule("java.xml").orElseThrow();
        Module base = String.class.getModule();

        System.out.println("java.sql membaca java.xml?  " + sql.canRead(xml));
        System.out.println("java.base membaca java.xml? " + base.canRead(xml));
        System.out.println("java.sql membaca java.base? " + sql.canRead(base));
    }
}

Output yang Dihasilkan:

text
java.sql membaca java.xml?  true
java.base membaca java.xml? false
java.sql membaca java.base? true

Bedah hasilnya: java.sql membaca java.xml karena deskriptornya memuat requires java.xml transitive; java.sql membaca java.base karena module mana pun membacanya; dan java.base — yang tidak requires apa pun selain ditambah implisit — tidak membaca java.xml. Akses lintas module membutuhkan dua syarat sekaligus: pengirim harus membaca penerima (requires), dan package yang dituju harus diekspor penerima (exports). Kurang satu saja, akses tertutup.

Ketika requires Kurang #

Bagaimana bila kita lupa menulis requires? Compiler menjelaskan dengan sangat jujur. Contoh berikut: module com.toko.kasir memakai class dari com.toko.harga tanpa menyatakan ketergantungan apa pun — module-info.java milik si kasir hanya berisi deklarasi nama kosong. Hasil kompilasinya kita jalankan sungguhan:

text Pesan error javac saat requires hilang
src/com.toko.kasir/com/toko/kasir/AplikasiKasir.java:3: error:
package com.toko.harga is not visible
import com.toko.harga.HitungHarga;
               ^
  (package com.toko.harga is declared in module com.toko.harga,
   but module com.toko.kasir does not read it)

Pesan itu menyebut kedua pihak beserta alasannya — kontras dengan ClassNotFoundException di classpath yang baru muncul saat runtime jauh dari konteks kesalahan. Menambahkan satu baris requires com.toko.harga; ke deskriptor kasir membuat kompilasi langsung berhasil.

Ekspor Terbatas dengan exports to #

Kadang sebuah package memang ingin dibuka, tetapi hanya untuk satu mitra terpercaya. Sintaks qualified export exports P to M; membatasi penerima:

java Ekspor terbatas pada module com.gudang.inti
module com.gudang.inti {
    exports com.gudang.inti;
    exports com.gudang.rahasia to com.gudang.ui;
}

Module com.gudang.ui — satu-satunya yang disebut — boleh mengimpor com.gudang.rahasia.KunciRahasia dan programnya berjalan normal, mencetak Jumlah: 42 serta Kunci : RAHASIA-123. Namun module lain yang sama-sama requires com.gudang.inti, misalnya com.gudang.pihakketiga, ditolak compiler:

text Pesan error untuk module tak diundang
src/com.gudang.pihakketiga/com/gudang/pihakketiga/Penyusup.java:3: error:
package com.gudang.rahasia does not exist
import com.gudang.rahasia.KunciRahasia;
                         ^

Bagi module yang tidak diundang, package terbatas itu sama sekali tidak terlihat — compiler melaporkannya seolah package tersebut tidak ada, bukan sekadar tidak bisa diakses. Qualified export lazim dipakai JDK sendiri untuk berbagi package internal antar module platform tanpa membukanya ke publik.

Dua kata kunci ini menutup separuh cerita modulasi: struktur dan visibilitas. Separuh sisanya mengurus pertanyaan yang lebih menarik — bagaimana module bisa saling bekerja sama tanpa saling mengenal implementasinya? Jawabannya adalah layanan: uses, provides, dan ServiceLoader, topik Chapter berikutnya.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

KejarModul.java

Unduh berkas
java
public class KejarModul {
    public static void main(String[] args) {
        ModuleLayer lapisan = ModuleLayer.boot();
        Module sql = lapisan.findModule("java.sql").orElseThrow();
        Module xml = lapisan.findModule("java.xml").orElseThrow();
        Module base = String.class.getModule();

        System.out.println("java.sql membaca java.xml?  " + sql.canRead(xml));
        System.out.println("java.base membaca java.xml? " + base.canRead(xml));
        System.out.println("java.sql membaca java.base? " + sql.canRead(base));
    }
}