Module bernama dideklarasikan lewat satu berkas khusus bernama module-info.java yang diletakkan di akar hierarki sumber module. Berkas ini adalah deskriptor module — daftar pernyataan ringkas yang dibaca compiler saat kompilasi dan JVM saat peluncuran. Isinya menentukan siapa boleh membaca siapa, package mana yang menjadi API publik, dan bagian mana yang dibuka untuk reflection. Pada Chapter ini kita akan membedah anatomi deskriptor secara utuh: dari requires, exports, opens, hingga uses dan provides, lalu membaca deskriptor module JDK sungguhan dari dalam program.

Anatomi module-info.java #

Bentuk lengkap deskriptor memuat lima kelompok direktif. Contoh berikut memakai nama fiktif com.toko.erm sebagai module aplikasi:

java module-info.java dengan seluruh direktif utama
module com.toko.erm {
    requires java.logging;               // 1. module yang dibaca
    requires transitive java.sql;        // 2. module yang ikut dibaca pemakai kita
    exports com.toko.erm.api;            // 3. API publik module ini
    exports com.toko.erm.spi to
        com.toko.erm.plugin;             //    ...khusus untuk satu module
    opens com.toko.erm.model;            // 4. dibuka untuk reflection
    uses com.toko.erm.spi.Alat;          // 5. layanan yang kita konsumsi
    provides com.toko.erm.spi.Alat       //    ...dan layanan yang kita pasok
        with com.toko.erm.internal.AlatBawaan;
}

Rangkuman perannya:

DirektifArti singkat
requires Nmodule ini membaca module N
requires transitive Npemakai module ini otomatis ikut membaca N
exports Ppackage P dapat diakses module lain
exports P to Mpackage P hanya untuk module M
opens Ppackage P boleh diakses reflection dari luar
uses Smodule ini mencari implementasi layanan S lewat ServiceLoader
provides S with Imodule ini menyediakan implementasi I untuk layanan S

Sekelompok package yang tidak muncul di exports mana pun otomatis menjadi internal — meskipun seluruh class-nya public, tidak ada module lain yang bisa mengimpornya.

requires dan requires transitive #

requires java.logging menyatakan: module ini membaca module java.logging, sehingga class-nya boleh memakai package yang diekspor java.logging. Kalimat “membaca” adalah istilah resmi JPMS — akses antarmodule hanya terjadi bila pengirim requires penerima.

Varian requires transitive menjawab masalah keterbacaan tidak langsung. Ketika API publik module kita, misalnya method yang mengembalikan tipe java.sql.Connection, menyebut tipe dari module lain, pemakai module kita sebenarnya juga butuh membaca java.sql. Tanpa transitive, setiap pemakai harus menebak dan mendeklarasikan requires java.sql sendiri — rantai dependensi tersembunyi. Dengan requires transitive java.sql, siapa pun yang requires com.toko.erm otomatis ikut membaca java.sql. Aturan praktisnya: pakai transitive untuk module yang tipenya muncul di API publik Anda, dan requires biasa untuk kebutuhan internal.

exports dan opens untuk Reflection #

exports membuka akses waktu kompilasi dan runtime biasa: module lain boleh mengimpor class dari package itu. Ada dua variasi penting. Pertama, qualified export exports com.toko.erm.spi to com.toko.erm.plugin; — package hanya terlihat oleh module yang disebut; kita gunakan ini pada Chapter requires dan exports. Kedua, opens. Reflection — mesin di balik framework seperti Jackson, Hibernate, dan Spring — sering harus mengintip field private untuk mengisi objek. Access biasa tidak cukup; package harus dibuka khusus:

java opens untuk framework ber-reflection
module com.toko.erm {
    requires java.logging;
    exports com.toko.erm.api;
    opens com.toko.erm.model;          // Jackson boleh memetakan class ini
    // com.toko.erm.internal tetap tertutup rapat
}

Perbedaan tajamnya: exports mengizinkan pemakaian terprogram (compile-time), sedangkan opens mengizinkan akses mendalam lewat setAccessible(true) di reflection. Package boleh exports tanpa opens (API terbuka tapi reflection ditolak), opens tanpa exports (hanya untuk framework), atau keduanya sekaligus.

uses dan provides #

Dua direktif terakhir mengurus layanan (service) — pola di mana module konsumen hanya mengenal interface, sementara implementasi ditemukan otomatis saat runtime. uses S menyatakan “module ini meminta implementasi layanan S ke ServiceLoader”, dan provides S with I menyatakan “module ini menyuplai implementasi I untuk layanan S”. Karena topik ini cukup kaya untuk satu chapter penuh, kita bahas tuntas beserta contoh yang berjalan pada Services dan Providers.

Membaca Deskriptor dari Dalam Program #

Class ModuleDescriptor di java.lang.module memungkinkan kita membaca isi deskriptor module mana pun saat runtime. Program berikut membongkar deskriptor java.sql milik JDK:

import java.lang.module.ModuleDescriptor;
import java.util.Comparator;

public class InfoDeskriptor {
    public static void main(String[] args) {
        ModuleDescriptor deskripsi = java.sql.Date.class.getModule().getDescriptor();

        System.out.println("nama     : " + deskripsi.name());
        System.out.println("requires :");
        deskripsi.requires().stream()
                .sorted(Comparator.comparing(ModuleDescriptor.Requires::name))
                .forEach(r -> {
                    String tanda = r.modifiers()
                            .contains(ModuleDescriptor.Requires.Modifier.TRANSITIVE)
                            ? " (transitive)" : "";
                    System.out.println("  " + r.name() + tanda);
                });
        System.out.println("exports  : " + deskripsi.exports().stream()
                .map(ModuleDescriptor.Exports::source)
                .sorted()
                .toList());
        System.out.println("uses     : " + deskripsi.uses());
    }
}

Output yang Dihasilkan:

text
nama     : java.sql
requires :
  java.base
  java.logging (transitive)
  java.transaction.xa (transitive)
  java.xml (transitive)
exports  : [java.sql, javax.sql]
uses     : [java.sql.Driver]

Mari kita bedah hasil cetaknya:

  1. java.base tanpa tanda: setiap module membaca java.base — di deskriptor java.sql ketergantungan itu ditulis eksplisit, pada module Anda sendiri JVM menambahkannya secara implisit.
  2. Tiga module bertanda transitive: tipe dari java.logging, java.transaction.xa, dan java.xml muncul di API publik JDBC, sehingga pemakai java.sql otomatis ikut membacanya — penerapan langsung aturan requires transitive di atas.
  3. exports [java.sql, javax.sql]: hanya dua package yang menjadi API; seluruh package internal java.sql (misalnya jdk.internal. milik implementasi) tidak tercantum dan tertutup bagi kita.
  4. uses [java.sql.Driver]: JDBC mencari driver database lewat ServiceLoader — kita akan melihat mekanisme lengkapnya pada Chapter Services dan Providers.
  5. Dua .sorted(...) pada stream: requires() dan exports() sama-sama mengembalikan Set yang urutan iterasinya tidak dijamin stabil antar eksekusi JVM; hasilnya kita urutkan berdasarkan nama agar output program dapat diandalkan dari eksekusi ke eksekusi — kebiasaan penting untuk setiap program yang mencetak isi koleksi tak berurut.

Menampilkan Deskriptor dengan java –describe-module #

Launcher memiliki opsi praktis untuk melihat deskriptor tanpa menulis kode — pada module sistem maupun module kita sendiri:

bash Deskriptor module java.sql
java --describe-module java.sql

Output yang Dihasilkan:

text
[email protected]
exports java.sql
exports javax.sql
requires java.base mandated
requires java.transaction.xa transitive
requires java.logging transitive
requires java.xml transitive
uses java.sql.Driver

Enam baris tengahnya persis isi module-info.java milik java.sql di kode sumber JDK — mandated menandai requires java.base yang memang diwajibkan spesifikasi. Opsi yang sama bekerja untuk module hasil kompilasi Anda sendiri cukup dengan menyebut module path-nya.

Deskriptor kini kita pahami sebagai kontrak tertulis. Chapter berikutnya menghidupkannya: kita akan menyusun dua module sungguhan, mengompilasinya dengan --module-source-path, mengamati graf dependensinya, lalu melihat error presisi yang muncul ketika requires atau exports kurang satu baris.

Referensi #

Berkas Sumber

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

InfoDeskriptor.java

Unduh berkas
java
import java.lang.module.ModuleDescriptor;
import java.util.Comparator;

public class InfoDeskriptor {
    public static void main(String[] args) {
        ModuleDescriptor deskripsi = java.sql.Date.class.getModule().getDescriptor();

        System.out.println("nama     : " + deskripsi.name());
        System.out.println("requires :");
        deskripsi.requires().stream()
                .sorted(Comparator.comparing(ModuleDescriptor.Requires::name))
                .forEach(r -> {
                    String tanda = r.modifiers()
                            .contains(ModuleDescriptor.Requires.Modifier.TRANSITIVE)
                            ? " (transitive)" : "";
                    System.out.println("  " + r.name() + tanda);
                });
        System.out.println("exports  : " + deskripsi.exports().stream()
                .map(ModuleDescriptor.Exports::source)
                .sorted()
                .toList());
        System.out.println("uses     : " + deskripsi.uses());
    }
}