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:
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:
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:
- Baris
Path berkasLayanan = ...hinggaFiles.writeString(...): program menulis sendiri berkasMETA-INF/services/Penerjemahberisi dua nama class. Pada proyek nyata berkas ini adalah berkas sumber statis disrc/main/resourcesyang disalin build tool ke JAR — di sini dibuat dari dalam program agar contoh berjalan mandiri tanpa langkah penyiapan terpisah. - Baris
ServiceLoader.load(Penerjemah.class):ServiceLoadermemindai seluruh classpath mencari berkasMETA-INF/services/Penerjemah; setiap baris di dalamnya dibaca sebagai nama class lalu diinstansiasi lewat constructor tanpa argumen. - Perulangan
for (Penerjemah p : penyedia): provider muncul sesuai urutan baris di berkas layanan. Aplikasi sama sekali tidak memanggilnew PenerjemahId(...)— pemilihan implementasi dipindahkan ke konfigurasi. - Nama berkas tanpa package: karena interface kita berada di default package, nama berkasnya cukup
Penerjemah; untuk interfacecom.toko.erm.spi.Alatberkasnya bernama persisMETA-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:
module com.pelanggan.api {
exports com.pelanggan.api;
}module com.pelanggan.fast {
requires com.pelanggan.api;
provides com.pelanggan.api.Penyapa
with com.pelanggan.fast.PenyapaCepat;
}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:
javac -d mods --module-source-path src <seluruh berkas .java dari tiga module>
java --module-path mods --module com.pelanggan.app/com.pelanggan.app.AplikasiUtamaOutput yang Dihasilkan:
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 #
- Java API: java.util.ServiceLoader
- JEP 261: Module System (ServiceLoader di module)
- dev.java: Java Modules
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
AplikasiPenerjemah.java
Unduh berkasimport 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 berkaspublic interface Penerjemah {
String terjemahkan(String teks);
}
PenerjemahFormal.java
Unduh berkaspublic class PenerjemahFormal implements Penerjemah {
@Override
public String terjemahkan(String teks) {
return "[formal] " + teks + ".";
}
}
PenerjemahId.java
Unduh berkaspublic class PenerjemahId implements Penerjemah {
@Override
public String terjemahkan(String teks) {
return "[id] " + teks;
}
}