requires dan exports mengikat module secara kaku: module A harus menyebut nama module B saat kompilasi. Namun banyak arsitektur justru menginginkan sebaliknya — konsumen hanya mengenal interface, dan implementasi hadir belakangan tanpa konsumen tahu asalnya. JDBC adalah contoh terbesarnya: API database hidup di module java.sql, sementara driver MySQL atau PostgreSQL dipasang terpisah dan ditemukan otomatis saat runtime. Mekanismenya adalah layanan (service): kontrak interface di satu sisi, provider implementasi di sisi lain, dan ServiceLoader sebagai pencari. Pada Chapter ini kita akan menjalankan pola itu di classpath, lalu melihat bentuk aslinya di module system.

Masalah yang Dipecahkan Layanan #

Bayangkan program penerjemahan yang ingin mendukung banyak penyedia terjemahan. Pendekatan langsung: class AplikasiPenerjemah menulis new PenerjemahId() — dan sejak detik itu aplikasi terikat keras pada satu implementasi. Menambah penyedia baru berarti mengedit kode aplikasi; pihak ketiga tidak bisa menambahkan penyedia tanpa menyentuh kode kita. Layanan membalik hubungan itu: aplikasi cukup menyatakan “saya memakai interface Penerjemah”, dan siapa pun boleh menyuplai class yang mengimplementasikannya. Pencarian dilakukan ServiceLoader berdasarkan berkas konfigurasi — bukan berdasarkan pemanggilan new.

ServiceLoader di Classpath #

Mekanisme layanan jauh lebih tua dari module system — ServiceLoader sudah hadir sejak Java 6 dan bekerja penuh di classpath. Konvensinya: untuk interface Penerjemah, provider didaftarkan lewat berkas teks di META-INF/services/Penerjemah yang isinya nama class implementasi, satu per baris. Interface dan implementasi kita tandai seperti biasa:

java Penerjemah.java
public interface Penerjemah {
    String terjemahkan(String teks);
}
public class PenerjemahId implements Penerjemah {
    @Override
    public String terjemahkan(String teks) {
        return "[id] " + teks;
    }
}
public class PenerjemahFormal implements Penerjemah {
    @Override
    public String terjemahkan(String teks) {
        return "[formal] " + teks + ".";
    }
}

Konsumen memuat seluruh provider terdaftar dengan ServiceLoader.load(Penerjemah.class):

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ServiceLoader;

public class AplikasiPenerjemah {
    public static void main(String[] args) throws IOException {
        Path berkasLayanan = Path.of("META-INF", "services", "Penerjemah");
        Files.createDirectories(berkasLayanan.getParent());
        Files.writeString(berkasLayanan, "PenerjemahId\nPenerjemahFormal\n",
                StandardCharsets.UTF_8);

        System.out.println("Berkas layanan : " + berkasLayanan);
        System.out.println("Isi            :");
        for (String baris : Files.readAllLines(berkasLayanan, StandardCharsets.UTF_8)) {
            System.out.println("  " + baris);
        }
        System.out.println();

        ServiceLoader<Penerjemah> penyedia = ServiceLoader.load(Penerjemah.class);
        for (Penerjemah p : penyedia) {
            System.out.println("Penyedia : " + p.getClass().getSimpleName());
            System.out.println("Hasil    : " + p.terjemahkan("selamat pagi"));
        }
    }
}

Output yang Dihasilkan:

text
Berkas layanan : META-INF/services/Penerjemah
Isi            :
  PenerjemahId
  PenerjemahFormal

Penyedia : PenerjemahId
Hasil    : [id] selamat pagi
Penyedia : PenerjemahFormal
Hasil    : [formal] selamat pagi.

Mari kita bedah bagian-bagian pentingnya:

  1. Baris Path berkasLayanan = ... hingga Files.writeString(...): program menulis sendiri berkas META-INF/services/Penerjemah berisi dua nama class. Pada proyek nyata berkas ini adalah berkas sumber statis di src/main/resources yang disalin build tool ke JAR — di sini dibuat dari dalam program agar contoh berjalan mandiri tanpa langkah penyiapan terpisah.
  2. Baris ServiceLoader.load(Penerjemah.class): ServiceLoader memindai seluruh classpath mencari berkas META-INF/services/Penerjemah; setiap baris di dalamnya dibaca sebagai nama class lalu diinstansiasi lewat constructor tanpa argumen.
  3. Perulangan for (Penerjemah p : penyedia): provider muncul sesuai urutan baris di berkas layanan. Aplikasi sama sekali tidak memanggil new PenerjemahId(...) — pemilihan implementasi dipindahkan ke konfigurasi.
  4. Nama berkas tanpa package: karena interface kita berada di default package, nama berkasnya cukup Penerjemah; untuk interface com.toko.erm.spi.Alat berkasnya bernama persis META-INF/services/com.toko.erm.spi.Alat.

Ganti isi berkas layanan — hapus satu baris atau tambah class baru — dan perilaku program berubah tanpa satu baris kode aplikasi disentuh. Berbagai library legendaris memakai pola persis ini untuk plugin.

Layanan dalam Module #

Di module system, pendaftaran provider naik kelas: dari berkas konfigurasi menjadi direktif provides ... with di module-info.java, dan konsumen menyatakan niatnya lewat uses. Kita susun tiga module — com.pelanggan.api memuat interface, com.pelanggan.fast menyuplai implementasi, com.pelanggan.app berperan konsumen:

java src/com.pelanggan.api/module-info.java
module com.pelanggan.api {
    exports com.pelanggan.api;
}
java src/com.pelanggan.fast/module-info.java
module com.pelanggan.fast {
    requires com.pelanggan.api;
    provides com.pelanggan.api.Penyapa
        with com.pelanggan.fast.PenyapaCepat;
}
java src/com.pelanggan.app/module-info.java
module com.pelanggan.app {
    requires com.pelanggan.api;
    uses com.pelanggan.api.Penyapa;
}

Perhatikan ketegasan peran: provides ... with pada module penyedia menggantikan berkas META-INF/services, uses pada module konsumen menggantikan pemanggilan ServiceLoader.load tanpa izin, dan yang paling menarik — module com.pelanggan.app tidak menulis requires com.pelanggan.fast. Konsumen tidak mengenal penyedia sama sekali; compiler tetap menerima kode konsumen meskipun penyedia belum ada, dan ServiceLoader menemukan implementasi lewat graf module saat runtime. Kompilasi dan eksekusinya kita jalankan sungguhan:

bash Kompilasi tiga module dan menjalankan konsumen
javac -d mods --module-source-path src <seluruh berkas .java dari tiga module>
java --module-path mods --module com.pelanggan.app/com.pelanggan.app.AplikasiUtama

Output yang Dihasilkan:

text
Penyedia : com.pelanggan.fast.PenyapaCepat
Hasil    : Halo, Budi! (penyapa cepat)

Konsumen mencetak nama class penyedia yang sebelumnya tidak pernah disebut kodenya — bukti bahwa pengikatan terjadi di runtime, bukan di kode sumber.

Keuntungan Arsitektur Layanan #

Pola kontrak-provider-pencari memberi tiga keuntungan nyata. Pertama, keterpisahan: interface, implementasi, dan konsumen boleh dikompilasi, diuji, dan didistribusikan sebagai unit terpisah. Kedua, ekstensibilitas pihak ketiga: vendor driver JDBC cukup mengemas class implementasi plus pendaftaran provides — aplikasi pengguna tidak berubah sama sekali. Ketiga, pemilihan saat penyiapan: operator memilih penyedia mana yang dipasang di lingkungan tanpa kompilasi ulang; memindahkan JAR penyedia keluar dari deployment secukupnya menonaktifkannya.

Layanan melengkapi kotak peralatan modulasi kita: struktur lewat requires/exports, fleksibilitas lewat uses/provides. Pertanyaan praktis terakhir menanti di Chapter berikutnya — bagaimana classpath dan module path berdampingan, dan bagaimana proyek lawas berpindah ke module system tanpa dirombak sekaligus.

Referensi #

Berkas Sumber

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

AplikasiPenerjemah.java

Unduh berkas
java
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ServiceLoader;

public class AplikasiPenerjemah {
    public static void main(String[] args) throws IOException {
        Path berkasLayanan = Path.of("META-INF", "services", "Penerjemah");
        Files.createDirectories(berkasLayanan.getParent());
        Files.writeString(berkasLayanan, "PenerjemahId\nPenerjemahFormal\n",
                StandardCharsets.UTF_8);

        System.out.println("Berkas layanan : " + berkasLayanan);
        System.out.println("Isi            :");
        for (String baris : Files.readAllLines(berkasLayanan, StandardCharsets.UTF_8)) {
            System.out.println("  " + baris);
        }
        System.out.println();

        ServiceLoader<Penerjemah> penyedia = ServiceLoader.load(Penerjemah.class);
        for (Penerjemah p : penyedia) {
            System.out.println("Penyedia : " + p.getClass().getSimpleName());
            System.out.println("Hasil    : " + p.terjemahkan("selamat pagi"));
        }
    }
}

Penerjemah.java

Unduh berkas
java
public interface Penerjemah {
    String terjemahkan(String teks);
}

PenerjemahFormal.java

Unduh berkas
java
public class PenerjemahFormal implements Penerjemah {
    @Override
    public String terjemahkan(String teks) {
        return "[formal] " + teks + ".";
    }
}

PenerjemahId.java

Unduh berkas
java
public class PenerjemahId implements Penerjemah {
    @Override
    public String terjemahkan(String teks) {
        return "[id] " + teks;
    }
}