Sepanjang Section ini kita memakai setAccessible(true) dengan bebas — membuka field privat Dompet, memanggil method tersembunyi, semuanya berjalan mulus. Kelancaran itu bukan hak tanpa syarat, melainkan konsekuensi kode contoh kita berdiri di unnamed module tanpa batasan module. Pada aplikasi nyata, setAccessible berada di persimpangan tiga kekhawatiran: ia melanggar kontrak enkapsulasi yang ditulis pembuat class, dibatasi keras oleh module system sejak Java 9, dan menjadi pintu yang dibuka pula oleh penyerang lewat deserialization serta framework injection. Chapter penutup ini membedah ketiganya, menunjukkan blokiran InaccessibleObjectException yang nyata, dan menutup dengan prinsip pemakaian reflection yang layak dipertahankan.
setAccessible Melanggar Kontrak Akses #
Modifier private adalah kontrak: pembuat class menyatakan bahwa hanya kode di dalam class itu yang boleh menyentuh anggotanya. setAccessible(true) membuat kode lain mengabaikan kontrak tersebut — invariant yang dijaga method privat (validasi, penguncian, pengecekan status) bisa dilewati begitu saja. Bahaya sebenarnya muncul ketika setAccessible diarahkan ke class milik pihak lain: pustaka yang mengandalkan field internalnya tetap konsisten bisa berperilaku rusak karena kode luar mengubah field yang seharusnya terkunci.
Java Platform Module System (JPMS), diresmikan lewat JEP 260, menjawab masalah ini dengan batas yang tidak bisa dilompati reflection biasa. Sejak Java 9, internals JDK seperti field value milik java.lang.Integer dikemas dalam module java.base yang tidak membuka package-nya. Mari kita buktikan — berkas berikut mencoba membaca nilai internal Integer:
import java.lang.reflect.Field;
public class AksesInteger {
public static void main(String[] args) throws Exception {
Integer kotak = 42;
Field field = Integer.class.getDeclaredField("value");
field.setAccessible(true); // mencoba membuka internals java.lang
int nilai = field.getInt(kotak);
System.out.println("nilai = " + nilai);
}
}JVM menolak mentah-mentah dengan exception yang kita verifikasi langsung:
Exception in thread "main" java.lang.reflect.InaccessibleObjectException: Unable to make field private final int java.lang.Integer.value accessible: module java.base does not "opens java.lang" to unnamed module @12028586Pesan tersebut padat informasi: JVM menyebut field yang dituju, module pemiliknya (java.base), package yang harus dibuka (java.lang), dan modul pemanggil yang belum diberi izin. Angka heksadesimal setelah unnamed module adalah identity hash class loader — nilainya bisa berbeda di lingkungan lain, tetapi substansi pesannya tetap. Perhatikan juga jenis errornya: InaccessibleObjectException, bukan IllegalAccessException dari Chapter Akses Fields — ini kegagalan kebijakan module, bukan sekadar kegagalan modifier.
Pintu Darurat –add-opens #
JVM menyediakan jalan keluar eksplisit lewat opsi --add-opens <module>/<package>=ALL-UNNAMED — memerintahkan JVM membuka satu package dari satu module kepada seluruh kode tanpa module, termasuk kode kita:
java --add-opens java.base/java.lang=ALL-UNNAMED AksesInteger.javaDengan opsi tersebut, eksekusi yang kita uji langsung berubah menjadi sukses dan mencetak nilai = 42. Opsi ini memang dibutuhkan sebagian pustaka lama yang mengandalkan internals JDK sebelum era module. Namun namanya tepat disebut pintu darurat: setiap --add-opens mencabut perlindungan satu package seluruhnya untuk seluruh kode tanpa nama, tanpa pemeriksaan lebih lanjut. Mengumpulkan --add-opens demi “cukup jalan” adalah utang yang jatuh tempo saat kode lain — termasuk kode berbahaya — menikmati pintu yang sama terbuka.
Pola Aman Reflection #
Cara sehat memakai reflection dapat dirangkum dua pola, dan keduanya bisa dijalankan tanpa satu pun --add-opens:
import java.lang.reflect.Field;
class Suhu {
private double celcius;
public Suhu(double celcius) {
this.celcius = celcius;
}
public double fahrenheit() {
return celcius * 9 / 5 + 32;
}
}
public class AksesAman {
public static void main(String[] args) throws Exception {
// Pola aman 1: refleksi pada class milik kode sendiri
Field celcius = Suhu.class.getDeclaredField("celcius");
celcius.setAccessible(true);
Suhu suhu = new Suhu(100);
System.out.println("celcius : " + celcius.getDouble(suhu));
// Pola aman 2: pakai API publik, bukan internals pihak lain
System.out.println("fahrenheit : " + suhu.fahrenheit());
Integer kotak = 42;
System.out.println("intValue : " + kotak.intValue());
// Introspeksi module tanpa membuka akses
Module sendiri = Suhu.class.getModule();
Module pustaka = Integer.class.getModule();
System.out.println("module Suhu : "
+ (sendiri.isNamed() ? sendiri.getName() : "(tanpa nama)"));
System.out.println("module Integer : "
+ (pustaka.isNamed() ? pustaka.getName() : "(tanpa nama)"));
}
}Output yang Dihasilkan:
celcius : 100.0
fahrenheit : 212.0
intValue : 42
module Suhu : (tanpa nama)
module Integer : java.baseTiga potong kode di atas mewakili tiga kebiasaan aman. Pertama, setAccessible pada class Suhu berhasil karena kode dan class berada di unnamed module yang sama — alasan contoh-contoh Section ini selalu mulus. Kedua, kebutuhan membaca Integer diselesaikan lewat API publik intValue() — pertanyaan “apa nilai kotak ini?” tidak pernah membutuhkan reflection. Ketiga, introspeksi module (getModule, isNamed, getName) membaca informasi tanpa membuka apa pun: kode kita tidak bernama karena berjalan dari classpath tanpa module-info, sedangkan Integer tinggal di module java.base.
Deserialization dan Framework Injection #
Dua jalur serangan nyata memakai reflection sebagai mesinnya. Jalur pertama adalah deserialization: ObjectInputStream membangun objek dari aliran byte dengan memanggil constructor dan mengisi field lewat reflection — tanpa bertanya apakah class tersebut aman dihidupkan ulang. Penyerang yang bisa menyuntikkan aliran byte ke cache, antrian pesan, atau endpoint jaringan dapat memicu gadget chain: rangkaian objek pustaka yang saat dibangun kembali menjalankan efek berbahaya, dari mengeksekusi perintah sistem sampai membuka koneksi keluar. Mitigasinya berlapis: jangan deserialisasi data dari pihak yang tidak dipercaya, dan gunakan filter masukan JEP 290 (ObjectInputFilter) yang menyaring class sebelum dihidupkan.
Jalur kedua adalah reflective injection pada framework. Dependency injection, ORM, dan plugin loader semuanya membangun objek dari nama class atau metadata — kode yang menentukan apa yang dibangun sama pentingnya dengan kode program itu sendiri. Konfigurasi yang bisa ditulis pihak luar (berkas terbuka, data request yang mencapai Class.forName) adalah celah injeksi class berbahaya. Prinsip minimalnya: nama class dan metadata pembangunan objek hanya boleh datang dari sumber terpercaya dan divalidasi terhadap daftar putih.
Prinsip penutupnya kembali ke pesan Chapter Reflection API: reflection untuk infrastruktur, bukan logika bisnis. Framework teruji yang memindai annotation, container yang membangun objek, dan alat yang menginspeksi struktur adalah pemakai sah yang membatasi refleksi pada lapisan tepi. Logika bisnis yang memanggil method lewat string, membuka field privat demi jalan pintas, atau mengandalkan internals pihak lain adalah tempat reflection berubah dari alat menjadi beban.
Dengan itu, perjalanan Section Annotations dan Reflection tuntas: dari lima annotation bawaan, definisi annotation kustom, retensi dan target, pemrosesan runtime, hingga kekuatan, biaya, dan batas keamanan reflection. Metadata dan inspeksi runtime kini menjadi kotak peralatan yang Anda pahami batas-batasnya — pemakaian yang tepat tinggal menunggu konteks framework pada Section berikutnya.
Referensi #
- Class java.lang.reflect.AccessibleObject (Oracle)
- Class java.lang.Module (Oracle)
- JEP 260: Encapsulate Most Internal APIs
- JEP 290: Filter Incoming Serialization Data
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
AksesAman.java
Unduh berkasimport java.lang.reflect.Field;
class Suhu {
private double celcius;
public Suhu(double celcius) {
this.celcius = celcius;
}
public double fahrenheit() {
return celcius * 9 / 5 + 32;
}
}
public class AksesAman {
public static void main(String[] args) throws Exception {
// Pola aman 1: refleksi pada class milik kode sendiri
Field celcius = Suhu.class.getDeclaredField("celcius");
celcius.setAccessible(true);
Suhu suhu = new Suhu(100);
System.out.println("celcius : " + celcius.getDouble(suhu));
// Pola aman 2: pakai API publik, bukan internals pihak lain
System.out.println("fahrenheit : " + suhu.fahrenheit());
Integer kotak = 42;
System.out.println("intValue : " + kotak.intValue());
// Introspeksi module tanpa membuka akses
Module sendiri = Suhu.class.getModule();
Module pustaka = Integer.class.getModule();
System.out.println("module Suhu : "
+ (sendiri.isNamed() ? sendiri.getName() : "(tanpa nama)"));
System.out.println("module Integer : "
+ (pustaka.isNamed() ? pustaka.getName() : "(tanpa nama)"));
}
}
AksesInteger.java
Unduh berkasimport java.lang.reflect.Field;
public class AksesInteger {
public static void main(String[] args) throws Exception {
Integer kotak = 42;
Field field = Integer.class.getDeclaredField("value");
field.setAccessible(true); // mencoba membuka internals java.lang
int nilai = field.getInt(kotak);
System.out.println("nilai = " + nilai);
}
}