Dasar-dasar pemuat kelas Java

Konsep pemuat kelas, salah satu landasan mesin virtual Java, menjelaskan perilaku mengubah kelas bernama menjadi bit yang bertanggung jawab untuk mengimplementasikan kelas itu. Karena class loader ada, run time Java tidak perlu mengetahui apa pun tentang file dan sistem file saat menjalankan program Java.

Apa yang dilakukan oleh pemuat kelas

Kelas diperkenalkan ke dalam lingkungan Java ketika mereka direferensikan dengan nama di kelas yang sudah berjalan. Ada sedikit keajaiban yang berlanjut untuk menjalankan kelas pertama (itulah sebabnya Anda harus mendeklarasikan metode main () sebagai statis, mengambil larik string sebagai argumen), tetapi begitu kelas itu berjalan, upaya selanjutnya di kelas pemuatan dilakukan oleh pemuat kelas.

Sederhananya, pemuat kelas membuat ruang nama datar dari badan kelas yang direferensikan dengan nama string. Definisi metode adalah:

Kelas r = loadClass (String className, boolean resolIt); 

Variabel className berisi string yang dipahami oleh pemuat kelas dan digunakan untuk mengidentifikasi implementasi kelas secara unik. Variabel menyelesaikan Ini adalah tanda untuk memberi tahu pemuat kelas bahwa kelas yang direferensikan oleh nama kelas ini harus diselesaikan (yaitu, kelas yang direferensikan harus dimuat juga).

Semua mesin virtual Java menyertakan satu pemuat kelas yang tertanam di mesin virtual. Loader yang disematkan ini disebut loader kelas primordial. Ini agak istimewa karena mesin virtual mengasumsikan bahwa ia memiliki akses ke repositori kelas tepercaya yang dapat dijalankan oleh VM tanpa verifikasi.

Loader kelas primordial mengimplementasikan implementasi default loadClass () . Dengan demikian, kode ini memahami bahwa nama kelas java.lang.Object disimpan dalam file dengan awalan java / lang / Object.class di suatu tempat di jalur kelas. Kode ini juga mengimplementasikan pencarian jalur kelas dan mencari file zip untuk kelas. Hal yang sangat keren tentang cara ini dirancang adalah bahwa Java dapat mengubah model penyimpanan kelasnya hanya dengan mengubah sekumpulan fungsi yang mengimplementasikan pemuat kelas.

Menggali dalam isi mesin virtual Java, Anda akan menemukan bahwa pemuat kelas primordial diimplementasikan terutama dalam fungsi FindClassFromClass dan ResolveClass .

Jadi, kapan kelas dimuat? Ada dua kasus tepat: saat bytecode baru dijalankan (misalnya, FooClass f = new FooClass () ;) dan saat bytecode membuat referensi statis ke kelas (misalnya, System. Out ).

Pemuat kelas non-primordial

"Terus?" Anda mungkin bertanya.

Mesin virtual Java memiliki kait di dalamnya untuk memungkinkan loader kelas yang ditentukan pengguna digunakan sebagai pengganti yang primordial. Lebih jauh lagi, karena pemuat kelas pengguna mendapat celah pertama pada nama kelas, pengguna dapat mengimplementasikan sejumlah repositori kelas yang menarik, tidak sedikit di antaranya adalah server HTTP - yang sejak awal membuat Java.

Akan tetapi, ada biaya, karena class loader sangat kuat (misalnya, dapat menggantikan java.lang.Object dengan versinya sendiri), class Java seperti applet tidak diizinkan untuk membuat instance pemuatnya sendiri. (Ngomong-ngomong, ini diberlakukan oleh pemuat kelas.) Kolom ini tidak akan berguna jika Anda mencoba melakukan hal ini dengan applet, hanya dengan aplikasi yang dijalankan dari repositori kelas tepercaya (seperti file lokal).

Loader kelas pengguna mendapat kesempatan untuk memuat kelas sebelum loader kelas primordial melakukannya. Karena itu, ia dapat memuat data implementasi kelas dari beberapa sumber alternatif, yaitu bagaimana AppletClassLoader dapat memuat kelas menggunakan protokol HTTP.

Membangun SimpleClassLoader

Pemuat kelas dimulai dengan menjadi subkelas dari java.lang.ClassLoader . Satu-satunya metode abstrak yang harus diimplementasikan adalah loadClass () . Alur loadClass () adalah sebagai berikut:

  • Verifikasi nama kelas.
  • Periksa untuk melihat apakah kelas yang diminta telah dimuat.
  • Periksa apakah kelas tersebut adalah kelas "sistem".
  • Mencoba mengambil kelas dari repositori pemuat kelas ini.
  • Tentukan kelas untuk VM.
  • Selesaikan kelas.
  • Kembalikan kelas ke penelepon.

SimpleClassLoader muncul sebagai berikut, dengan deskripsi tentang apa yang dilakukannya diselingi dengan kode.

kelas tersinkronisasi publik loadClass (String className, boolean resolIt) melempar ClassNotFoundException {Hasil kelas; byte classData []; System.out.println (">>>>>> Memuat kelas:" + className); / * Periksa cache lokal kelas kami * / result = (Class) class.get (className); if (result! = null) {System.out.println (">>>>>> mengembalikan hasil cache."); hasil pengembalian; }

Kode di atas adalah bagian pertama dari metode loadClass . Seperti yang Anda lihat, ini mengambil nama kelas dan mencari tabel hash lokal yang dijaga oleh pemuat kelas dari kelas yang telah dikembalikannya. Penting untuk menyimpan tabel hash ini karena Anda harus mengembalikan referensi objek kelas yang sama untuk nama kelas yang sama setiap kali Anda memintanya. Jika tidak, sistem akan percaya bahwa ada dua kelas berbeda dengan nama yang sama dan akan memunculkan ClassCastException setiap kali Anda menetapkan referensi objek di antara keduanya. Penting juga untuk menyimpan cache karena loadClass () metode dipanggil secara rekursif ketika kelas sedang diselesaikan, dan Anda perlu mengembalikan hasil yang di-cache daripada mengejarnya untuk salinan lain.

/ * Periksa dengan primordial class loader * / coba {result = super.findSystemClass (className); System.out.println (">>>>>> mengembalikan kelas sistem (dalam CLASSPATH)."); hasil pengembalian; } catch (ClassNotFoundException e) {System.out.println (">>>>>> Bukan kelas sistem."); }

Seperti yang Anda lihat pada kode di atas, langkah selanjutnya adalah memeriksa apakah loader kelas primordial dapat menyelesaikan nama kelas ini. Pemeriksaan ini penting untuk kewarasan dan keamanan sistem. Misalnya, jika Anda mengembalikan instance java.lang.Object Anda sendiri ke pemanggil, maka objek ini tidak akan berbagi superclass umum dengan objek lain! Keamanan sistem dapat dikompromikan jika pemuat kelas Anda mengembalikan nilai java.lang.SecurityManager sendiri , yang tidak memiliki pemeriksaan yang sama seperti yang asli.

/ * Coba muat dari repositori kami * / classData = getClassImplFromDataBase (className); if (classData == null) {throw new ClassNotFoundException (); }

Setelah pemeriksaan awal, kita sampai pada kode di atas yang mana loader kelas sederhana mendapat kesempatan untuk memuat implementasi kelas ini. The SimpleClassLoader memiliki metode getClassImplFromDataBase () yang dalam contoh sederhana kami hanya prefiks direktori "toko \" untuk nama kelas dan menambahkan ekstensi ".impl". Saya memilih teknik ini dalam contoh sehingga tidak akan ada pertanyaan tentang loader kelas primordial yang menemukan kelas kita. Perhatikan bahwa sun.applet.AppletClassLoader mengawali URL basis kode dari halaman HTML tempat applet berada di namanya, lalu melakukan permintaan HTTP get untuk mengambil bytecode.

 / * Definisikan (parsing file kelas) * / result = defineClass (classData, 0, classData.length); 

Jika implementasi kelas dimuat, langkah terakhir adalah memanggil metode defineClass () dari java.lang.ClassLoader , yang dapat dianggap sebagai langkah pertama verifikasi kelas. Metode ini diterapkan di mesin virtual Java dan bertanggung jawab untuk memverifikasi bahwa byte kelas adalah file kelas Java yang sah. Secara internal, metode defineClass mengisi struktur data yang digunakan JVM untuk menampung kelas. Jika data kelas salah format , panggilan ini akan menyebabkan ClassFormatError dilempar.

if (resolIt) {resolClass (result); }

Persyaratan khusus loader kelas terakhir adalah memanggil resolClass () jika parameter boolean resolIt benar. Metode ini melakukan dua hal: Pertama, menyebabkan setiap kelas yang dirujuk oleh kelas ini secara eksplisit dimuat dan objek prototipe untuk kelas ini dibuat; kemudian, itu memanggil pemverifikasi untuk melakukan verifikasi dinamis keabsahan bytecode di kelas ini. Jika verifikasi gagal, panggilan metode ini akan memunculkan LinkageError , yang paling umum adalah VerifyError .

Note that for any class you will load, the resolveIt variable will always be true. It is only when the system is recursively calling loadClass() that it may set this variable false because it knows the class it is asking for is already resolved.

 classes.put(className, result); System.out.println(" >>>>>> Returning newly loaded class."); return result; } 

The final step in the process is to store the class we've loaded and resolved into our hash table so that we can return it again if need be, and then to return the Class reference to the caller.

Of course if it were this simple there wouldn't be much more to talk about. In fact, there are two issues that class loader builders will have to deal with, security and talking to classes loaded by the custom class loader.

Security considerations

Whenever you have an application loading arbitrary classes into the system through your class loader, your application's integrity is at risk. This is due to the power of the class loader. Let's take a moment to look at one of the ways a potential villain could break into your application if you aren't careful.

In our simple class loader, if the primordial class loader couldn't find the class, we loaded it from our private repository. What happens when that repository contains the class java.lang.FooBar ? There is no class named java.lang.FooBar, but we could install one by loading it from the class repository. This class, by virtue of the fact that it would have access to any package-protected variable in the java.lang package, can manipulate some sensitive variables so that later classes could subvert security measures. Therefore, one of the jobs of any class loader is to protect the system name space.

In our simple class loader we can add the code:

 if (className.startsWith("java.")) throw newClassNotFoundException(); 

just after the call to findSystemClass above. This technique can be used to protect any package where you are sure that the loaded code will never have a reason to load a new class into some package.

Another area of risk is that the name passed must be a verified valid name. Consider a hostile application that used a class name of "..\..\..\..\netscape\temp\xxx.class" as its class name that it wanted loaded. Clearly, if the class loader simply presented this name to our simplistic file system loader this might load a class that actually wasn't expected by our application. Thus, before searching our own repository of classes, it is a good idea to write a method that verifies the integrity of your class names. Then call that method just before you go to search your repository.

Using an interface to bridge the gap

The second non-intuitive issue with working with class loaders is the inability to cast an object that was created from a loaded class into its original class. You need to cast the object returned because the typical use of a custom class loader is something like:

 CustomClassLoader ccl = new CustomClassLoader(); Object o; Class c; c = ccl.loadClass("someNewClass"); o = c.newInstance(); ((SomeNewClass)o).someClassMethod(); 

However, you cannot cast o to SomeNewClass because only the custom class loader "knows" about the new class it has just loaded.

There are two reasons for this. First, the classes in the Java virtual machine are considered castable if they have at least one common class pointer. However, classes loaded by two different class loaders will have two different class pointers and no classes in common (except java.lang.Object usually). Second, the idea behind having a custom class loader is to load classes after the application is deployed so the application does not know a priory about the classes it will load. This dilemma is solved by giving both the application and the loaded class a class in common.

There are two ways of creating this common class, either the loaded class must be a subclass of a class that the application has loaded from its trusted repository, or the loaded class must implement an interface that was loaded from the trusted repository. This way the loaded class and the class that does not share the complete name space of the custom class loader have a class in common. In the example I use an interface named LocalModule, although you could just as easily make this a class and subclass it.

Contoh terbaik dari teknik pertama adalah browser Web. Kelas yang didefinisikan oleh Java yang diimplementasikan oleh semua applet adalah java.applet.Applet . Ketika kelas dimuat oleh AppletClassLoader , instance objek yang dibuat akan dilemparkan ke instance Applet . Jika cast ini berhasil, metode init () dipanggil. Dalam contoh saya, saya menggunakan teknik kedua, antarmuka.

Bermain dengan contoh

Untuk melengkapi contoh, saya telah membuat beberapa lagi

.Jawa

file. Ini adalah:

antarmuka publik LocalModule {/ * Mulai modul * / void start (opsi String); }