Lihat di dalam kelas Java

Selamat datang di angsuran "Java In Depth" bulan ini. Salah satu tantangan paling awal untuk Java adalah apakah Java dapat berdiri sebagai bahasa "sistem" yang mumpuni atau tidak. Akar dari pertanyaan tersebut melibatkan fitur keamanan Java yang mencegah kelas Java mengetahui kelas lain yang berjalan di sampingnya di mesin virtual. Kemampuan untuk "melihat ke dalam" kelas ini disebut introspeksi . Dalam rilis Java publik pertama, yang dikenal sebagai Alpha3, aturan bahasa yang ketat terkait visibilitas komponen internal kelas dapat dielakkan melalui penggunaan ObjectScopekelas. Kemudian, selama beta, ketika ObjectScopedihapus dari run time karena masalah keamanan, banyak orang menyatakan Java tidak cocok untuk pengembangan "serius".

Mengapa introspeksi diperlukan agar suatu bahasa dianggap sebagai bahasa "sistem"? Salah satu bagian dari jawabannya cukup biasa: Mendapatkan dari "tidak ada" (yaitu, VM yang tidak diinisialisasi) ke "sesuatu" (yaitu, kelas Java yang sedang berjalan) mengharuskan beberapa bagian sistem dapat memeriksa kelas yang akan lari untuk mencari tahu apa yang harus dilakukan dengan mereka. Contoh kanonik dari masalah ini adalah sebagai berikut: "Bagaimana sebuah program, yang ditulis dalam bahasa yang tidak dapat melihat 'di dalam' komponen bahasa lain, mulai mengeksekusi komponen bahasa pertama, yang merupakan titik awal eksekusi untuk semua komponen lainnya? "

Ada dua cara untuk menangani introspeksi di Java: inspeksi file kelas dan API refleksi baru yang merupakan bagian dari Java 1.1.x. Saya akan membahas kedua teknik tersebut, tetapi di kolom ini saya akan fokus pada pemeriksaan file kelas satu. Di kolom mendatang saya akan melihat bagaimana API refleksi menyelesaikan masalah ini. (Tautan ke kode sumber lengkap untuk kolom ini tersedia di bagian Sumber.)

Lihat lebih dalam ke file saya ...

Dalam rilis 1.0.x Java, salah satu kutukan terbesar pada run time Java adalah cara Java menjalankan program. Apa masalahnya? Eksekusi sedang transit dari domain sistem operasi host (Win 95, SunOS, dan seterusnya) ke dalam domain mesin virtual Java. Mengetik baris " java MyClass arg1 arg2" akan menggerakkan serangkaian peristiwa yang sepenuhnya diprogram secara langsung oleh penerjemah Java.

Sebagai kejadian pertama, shell perintah sistem operasi memuat interpreter Java dan meneruskannya dengan string "MyClass arg1 arg2" sebagai argumennya. Peristiwa berikutnya terjadi ketika penerjemah Java mencoba menemukan kelas yang bernama MyClassdi salah satu direktori yang diidentifikasi di jalur kelas. Jika kelas ditemukan, acara ketiga adalah untuk menemukan metode di dalam kelas bernama main, yang tanda tangannya memiliki pengubah "publik" dan "statis" dan yang mengambil array Stringobjek sebagai argumennya. Jika metode ini ditemukan, thread primordial dibuat dan metode tersebut dipanggil. Interpreter Java kemudian mengubah "arg1 arg2" menjadi larik string. Setelah metode ini dipanggil, yang lainnya adalah Java murni.

Ini semua baik dan bagus kecuali bahwa mainmetode tersebut harus statis karena waktu proses tidak dapat memanggilnya dengan lingkungan Java yang belum ada. Selanjutnya, metode pertama harus diberi nama mainkarena tidak ada cara untuk memberi tahu interpreter nama metode pada baris perintah. Bahkan jika Anda memberi tahu penerjemah nama metode tersebut, tidak ada cara umum untuk mengetahui apakah metode itu ada di kelas yang Anda beri nama pada awalnya. Terakhir, karena mainmetodenya statis, Anda tidak dapat mendeklarasikannya di antarmuka, dan itu berarti Anda tidak dapat menentukan antarmuka seperti ini:

Aplikasi antarmuka publik {public void main (String args []); }

Jika antarmuka di atas telah didefinisikan, dan kelas-kelas mengimplementasikannya, maka setidaknya Anda dapat menggunakan instanceofoperator di Java untuk menentukan apakah Anda memiliki aplikasi atau tidak dan dengan demikian menentukan apakah itu cocok untuk dipanggil dari baris perintah. Intinya adalah Anda tidak bisa (mendefinisikan antarmuka), itu tidak (dibangun ke dalam interpreter Java), dan Anda tidak bisa (menentukan apakah file kelas adalah aplikasi dengan mudah). Jadi apa yang bisa kamu lakukan?

Sebenarnya, Anda bisa melakukan sedikit jika Anda tahu apa yang harus dicari dan bagaimana menggunakannya.

Mendekompilasi file kelas

File kelas Java adalah arsitektur-netral, yang berarti itu adalah kumpulan bit yang sama baik itu dimuat dari mesin Windows 95 atau mesin Sun Solaris. Ini juga didokumentasikan dengan sangat baik dalam buku The Java Virtual Machine Specification oleh Lindholm dan Yellin. Struktur file kelas dirancang, sebagian, agar mudah dimuat ke dalam ruang alamat SPARC. Pada dasarnya, file kelas dapat dipetakan ke dalam ruang alamat virtual, kemudian penunjuk relatif di dalam kelas diperbaiki, dan presto! Anda memiliki struktur kelas instan. Ini kurang berguna pada mesin arsitektur Intel, tetapi warisan membuat format file kelas mudah dipahami, dan bahkan lebih mudah untuk dipecah.

Pada musim panas tahun 1994, saya bekerja di grup Java dan membangun apa yang dikenal sebagai model keamanan "hak paling rendah" untuk Java. Saya baru saja selesai mencari tahu bahwa yang benar-benar ingin saya lakukan adalah melihat ke dalam kelas Java, memotong bagian-bagian yang tidak diizinkan oleh tingkat hak istimewa saat ini, dan kemudian memuat hasilnya melalui pemuat kelas khusus. Saat itulah saya menemukan tidak ada kelas dalam run time utama yang mengetahui tentang pembuatan file kelas. Ada versi dalam pohon kelas kompilator (yang harus menghasilkan file kelas dari kode yang dikompilasi), tetapi saya lebih tertarik untuk membuat sesuatu untuk memanipulasi file kelas yang sudah ada sebelumnya.

Saya mulai dengan membangun kelas Java yang dapat mendekomposisi file kelas Java yang disajikan padanya pada aliran input. Saya memberinya nama yang kurang dari aslinya ClassFile. Awal dari kelas ini ditunjukkan di bawah.

public class ClassFile {int magic; majorVersion pendek; minorVersion pendek; ConstantPoolInfo constantPool []; akses singkatBendera; ConstantPoolInfo thisClass; ConstantPoolInfo superClass; Antarmuka ConstantPoolInfo []; Bidang FieldInfo []; Metode MethodInfo []; Atribut AttributeInfo []; boolean isValidClass = false; ACC_PUBLIC public int static final = 0x1; ACC_PRIVATE public int static final = 0x2; ACC_PROTECTED public int static final = 0x4; ACC_STATIC public int static final = 0x8; ACC_FINAL public int static final = 0x10; ACC_SYNCHRONIZED public int static final = 0x20; ACC_THREADSAFE public int static final = 0x40; ACC_TRANSIENT public int static final = 0x80; ACC_NATIVE public int static final = 0x100; ACC_INTERFACE = 0x200 public int static final; ACC_ABSTRACT public int static final = 0x400;

Seperti yang Anda lihat, variabel instan untuk kelas ClassFilemendefinisikan komponen utama dari file kelas Java. Secara khusus, struktur data pusat untuk file kelas Java dikenal sebagai kumpulan konstan. Potongan file kelas lain yang menarik mendapatkan kelasnya sendiri: MethodInfountuk metode, FieldInfountuk bidang (yang merupakan deklarasi variabel di kelas), AttributeInfountuk menyimpan atribut file kelas, dan satu set konstanta yang diambil langsung dari spesifikasi pada file kelas ke mendekode berbagai pengubah yang berlaku untuk bidang, metode, dan deklarasi kelas.

Metode utama kelas ini adalah read, yang digunakan untuk membaca file kelas dari disk dan membuat ClassFileinstance baru dari data. Kode untuk readmetode tersebut ditampilkan di bawah ini. Deskripsi saya selingi dengan kode karena metode ini cenderung cukup panjang.

1 pembacaan boolean publik (InputStream in) 2 melempar IOException {3 DataInputStream di = new DataInputStream (in); 4 hitungan int; 5 6 ajaib = di.readInt (); 7 if (magic! = (Int) 0xCAFEBABE) {8 return (false); 9} 10 11 majorVersion = di.readShort (); 12 minorVersion = di.readShort (); 13 hitungan = di.readShort (); 14 constantPool = new ConstantPoolInfo [hitungan]; 15 if (debug) 16 System.out.println ("read (): Read header ..."); 17 constantPool [0] = new ConstantPoolInfo (); 18 untuk (int i = 1; i <constantPool.length; i ++) {19 constantPool [i] = new ConstantPoolInfo (); 20 if (! ConstantPool [i] .read (di)) {21 return (false); 22} 23 // Kedua tipe ini mengambil "dua" tempat di tabel 24 if ((constantPool [i] .type == ConstantPoolInfo.LONG) || 25 (constantPool [i] .type == ConstantPoolInfo.DOUBLE)) 26 i ++; 27}

Seperti yang Anda lihat, kode di atas dimulai dengan terlebih dahulu membungkus DataInputStreamaliran input yang direferensikan oleh variabel in . Selanjutnya, di baris 6 sampai 12, semua informasi yang diperlukan untuk menentukan bahwa kode memang melihat file kelas yang valid ada. Informasi ini terdiri dari "cookie" ajaib 0xCAFEBABE, dan nomor versi 45 dan 3 masing-masing untuk nilai mayor dan minor. Selanjutnya, di baris 13 hingga 27, kumpulan konstanta dibaca menjadi larik ConstantPoolInfoobjek. Kode sumber menjadi ConstantPoolInfobiasa-biasa saja - ia hanya membaca data dan mengidentifikasinya berdasarkan tipenya. Elemen selanjutnya dari kumpulan konstan digunakan untuk menampilkan informasi tentang kelas.

Mengikuti kode di atas, readmetode akan memindai ulang kumpulan konstan dan referensi "memperbaiki" di kumpulan konstan yang merujuk ke item lain di kumpulan konstan. Kode perbaikan ditunjukkan di bawah ini. Perbaikan ini diperlukan karena referensi biasanya berupa indeks ke dalam kumpulan konstan, dan indeks tersebut sudah diselesaikan. Ini juga menyediakan pemeriksaan bagi pembaca untuk mengetahui bahwa file kelas tidak rusak pada tingkat kumpulan konstan.

28 untuk (int i = 1; i 0) 32 konstantaPool [i] .arg1 = konstantaPool [konstantaPool [i] .index1]; 33 jika (ConstantPool [i] .index2> 0) 34 ConstantPool [i] .arg2 = ConstantPool [ConstantPool [i] .index2]; 35} 36 37 if (dumpConstants) {38 untuk (int i = 1; i <constantPool.length; i ++) {39 System.out.println ("C" + i + "-" + constantPool [i]); 30} 31}

Dalam kode di atas, setiap entri kumpulan konstan menggunakan nilai indeks untuk mengetahui referensi ke entri kumpulan konstan lainnya. Ketika selesai di baris 36, seluruh kumpulan secara opsional dibuang.

Setelah kode dipindai melewati kumpulan konstan, file kelas mendefinisikan informasi kelas utama: nama kelasnya, nama kelas super, dan antarmuka implementasi. The membaca scan kode untuk nilai-nilai ini seperti yang ditunjukkan di bawah ini.

32 accessFlags = di.readShort (); 33 34 thisClass = constantPool [di.readShort ()]; 35 superClass = constantPool [di.readShort ()]; 36 if (debug) 37 System.out.println ("read (): Read class info ..."); 38 39 / * 30 * Identifikasi semua antarmuka yang diimplementasikan oleh kelas ini 31 * / 32 count = di.readShort (); 33 jika (hitung! = 0) {34 if (debug) 35 System.out.println ("Kelas mengimplementasikan" + hitung + "antarmuka."); 36 antarmuka = ​​new ConstantPoolInfo [count]; 37 untuk (int i = 0; i <count; i ++) {38 int iindex = di.readShort (); 39 if ((iindex constantPool.length - 1)) 40 return (false); 41 antarmuka [i] = konstantaPool [iindex]; 42 if (debug) 43 System.out.println ("I" + i + ":" + interfaces [i]); 44} 45} 46 if (debug) 47 System.out.println ("read (): Read interface info ...");

Setelah kode ini selesai, readmetode telah membangun ide yang cukup bagus tentang struktur kelas. Yang tersisa hanyalah mengumpulkan definisi bidang, definisi metode, dan, mungkin yang terpenting, atribut file kelas.

Format file kelas memecah masing-masing dari ketiga grup ini menjadi bagian yang terdiri dari angka, diikuti dengan jumlah instance dari hal yang Anda cari. Jadi, untuk bidang, file kelas memiliki jumlah bidang yang ditentukan, dan kemudian banyak definisi bidang. Kode untuk memindai di lapangan ditunjukkan di bawah ini.

48 count = di.readShort(); 49 if (debug) 50 System.out.println("This class has "+count+" fields."); 51 if (count != 0) { 52 fields = new FieldInfo[count]; 53 for (int i = 0; i < count; i++) { 54 fields[i] = new FieldInfo(); 55 if (! fields[i].read(di, constantPool)) { 56 return (false); 57 } 58 if (debug) 59 System.out.println("F"+i+": "+ 60 fields[i].toString(constantPool)); 61 } 62 } 63 if (debug) 64 System.out.println("read(): Read field info..."); 

Kode di atas dimulai dengan membaca hitungan di baris # 48, kemudian, sementara hitungannya bukan nol, ia membaca di bidang baru menggunakan FieldInfokelas. The FieldInfokelas hanya mengisi data yang mendefinisikan field ke mesin virtual Java. Kode untuk membaca metode dan atribut adalah sama, hanya mengganti referensi FieldInfodengan referensi MethodInfoatau yang AttributeInfosesuai. Sumber itu tidak disertakan di sini, namun Anda dapat melihat sumbernya menggunakan tautan di bagian Sumber di bawah.