Die Grundlagen von Java-Klassenladeprogrammen

Das Klassenladekonzept, einer der Eckpfeiler der virtuellen Java-Maschine, beschreibt das Verhalten beim Konvertieren einer benannten Klasse in die Bits, die für die Implementierung dieser Klasse verantwortlich sind. Da Klassenlader vorhanden sind, muss die Java-Laufzeit beim Ausführen von Java-Programmen nichts über Dateien und Dateisysteme wissen.

Was Klassenlader tun

Klassen werden in die Java-Umgebung eingeführt, wenn sie in einer bereits ausgeführten Klasse namentlich referenziert werden. Es gibt ein bisschen Magie, um die erste Klasse zum Laufen zu bringen (weshalb Sie die main () -Methode als statisch deklarieren müssen , wobei ein String-Array als Argument verwendet wird), aber sobald diese Klasse ausgeführt wird, werden zukünftige Versuche unternommen Das Laden von Klassen erfolgt durch den Klassenlader.

Im einfachsten Fall erstellt ein Klassenladeprogramm einen flachen Namensraum von Klassenkörpern, auf die durch einen Zeichenfolgennamen verwiesen wird. Die Methodendefinition lautet:

Klasse r = loadClass (String className, boolean resolveIt); 

Die Variable className enthält eine Zeichenfolge, die vom Klassenladeprogramm verstanden wird und zur eindeutigen Identifizierung einer Klassenimplementierung verwendet wird. Die Variable resolveIt ist ein Flag, das dem Klassenladeprogramm mitteilt, dass Klassen, auf die durch diesen Klassennamen verwiesen wird, aufgelöst werden sollen ( dh , auf die auch verwiesen wird).

Alle virtuellen Java-Maschinen enthalten einen Klassenlader, der in die virtuelle Maschine eingebettet ist. Dieser eingebettete Loader wird als primordialer Klassenlader bezeichnet. Dies ist etwas Besonderes, da die virtuelle Maschine davon ausgeht, dass sie Zugriff auf ein Repository vertrauenswürdiger Klassen hat, die von der VM ohne Überprüfung ausgeführt werden können.

Der ursprüngliche Klassenladeprogramm implementiert die Standardimplementierung von loadClass () . Daher versteht dieser Code, dass der Klassenname java.lang.Object in einer Datei mit dem Präfix java / lang / Object.class irgendwo im Klassenpfad gespeichert ist. Dieser Code implementiert auch die Suche nach Klassenpfaden und das Durchsuchen von Zip-Dateien nach Klassen. Das wirklich Coole an der Art und Weise, wie dies entworfen wurde, ist, dass Java sein Klassenspeichermodell einfach durch Ändern des Funktionssatzes ändern kann, der den Klassenlader implementiert.

Wenn Sie sich in den Eingeweiden der Java Virtual Machine umsehen , werden Sie feststellen, dass der ursprüngliche Klassenladeprogramm hauptsächlich in den Funktionen FindClassFromClass und ResolveClass implementiert ist .

Wann werden Klassen geladen? Es gibt genau zwei Fälle: Wenn der neue Bytecode ausgeführt wird (z. B. FooClass f = new FooClass () ;) und wenn die Bytecodes einen statischen Verweis auf eine Klasse enthalten (z. B. System. Out ).

Ein nicht primordialer Klassenlader

"Na und?" du könntest fragen.

Die Java Virtual Machine verfügt über Hooks, mit denen ein benutzerdefinierter Klassenlader anstelle des ursprünglichen verwendet werden kann. Da der Benutzerklassenlader beim Klassennamen den ersten Riss bekommt, kann der Benutzer außerdem eine beliebige Anzahl interessanter Klassenrepositorys implementieren, nicht zuletzt HTTP-Server, die Java in erster Linie auf den Weg gebracht haben.

Da der Klassenlader jedoch so leistungsfähig ist (z. B. kann er java.lang.Object durch eine eigene Version ersetzen ), sind Java-Klassen wie Applets nicht berechtigt, ihre eigenen Lader zu instanziieren. (Dies wird übrigens vom Klassenladeprogramm erzwungen.) Diese Spalte ist nicht hilfreich, wenn Sie versuchen, diese Aufgaben mit einem Applet auszuführen, sondern nur mit einer Anwendung, die über das vertrauenswürdige Klassenrepository ausgeführt wird (z. B. lokale Dateien).

Ein Benutzerklassenlader hat die Möglichkeit, eine Klasse zu laden, bevor dies der ursprüngliche Klassenlader tut. Aus diesem Grund können die Klassenimplementierungsdaten aus einer anderen Quelle geladen werden. Auf diese Weise kann der AppletClassLoader Klassen mithilfe des HTTP-Protokolls laden.

Erstellen eines SimpleClassLoader

Ein Klassenladeprogramm ist zunächst eine Unterklasse von java.lang.ClassLoader . Die einzige abstrakte Methode, die implementiert werden muss, ist loadClass () . Der Ablauf von loadClass () ist wie folgt:

  • Überprüfen Sie den Klassennamen.
  • Überprüfen Sie, ob die angeforderte Klasse bereits geladen wurde.
  • Überprüfen Sie, ob die Klasse eine "System" -Klasse ist.
  • Versuchen Sie, die Klasse aus dem Repository dieses Klassenladeprogramms abzurufen.
  • Definieren Sie die Klasse für die VM.
  • Löse die Klasse auf.
  • Geben Sie die Klasse an den Aufrufer zurück.

SimpleClassLoader wird wie folgt angezeigt, mit Beschreibungen darüber, was es mit dem Code durchsetzt.

public synchronized Class loadClass (String className, boolean resolveIt) löst ClassNotFoundException aus {Class result; Byte classData []; System.out.println (">>>>>> Klasse laden:" + className); / * Überprüfen Sie unseren lokalen Klassencache * / result = (Class) classes.get (className); if (result! = null) {System.out.println (">>>>>> zwischengespeichertes Ergebnis zurückgeben."); Ergebnis zurückgeben; }}

Der obige Code ist der erste Abschnitt der loadClass- Methode. Wie Sie sehen können, wird ein Klassenname verwendet und eine lokale Hash-Tabelle durchsucht, die unser Klassenladeprogramm für bereits zurückgegebene Klassen verwaltet. Es ist wichtig , diese Hash - Tabelle um zu halten , da Sie müssen die gleiche Klasse Objektreferenz für die gleichen Klassennamen jedes Mal , wenn Sie gefragt werden , für sie zurück. Andernfalls glaubt das System, dass es zwei verschiedene Klassen mit demselben Namen gibt, und löst eine ClassCastException aus, wenn Sie eine Objektreferenz zwischen ihnen zuweisen. Es ist auch wichtig, einen Cache zu behalten, da die loadClass () Die Methode wird rekursiv aufgerufen, wenn eine Klasse aufgelöst wird, und Sie müssen das zwischengespeicherte Ergebnis zurückgeben, anstatt es für eine andere Kopie zu verfolgen.

/ * Erkundige dich beim ursprünglichen Klassenlader * / try {result = super.findSystemClass (className); System.out.println (">>>>>> Rückgabe der Systemklasse (in CLASSPATH)."); Ergebnis zurückgeben; } catch (ClassNotFoundException e) {System.out.println (">>>>>> Keine Systemklasse."); }}

Wie Sie im obigen Code sehen können, besteht der nächste Schritt darin, zu überprüfen, ob der ursprüngliche Klassenlader diesen Klassennamen auflösen kann. Diese Überprüfung ist sowohl für die Gesundheit als auch für die Sicherheit des Systems von wesentlicher Bedeutung. Wenn Sie beispielsweise Ihre eigene Instanz von java.lang.Object an den Aufrufer zurückgeben, teilt dieses Objekt keine gemeinsame Oberklasse mit einem anderen Objekt! Die Sicherheit des Systems kann beeinträchtigt werden, wenn Ihr Klassenladeprogramm seinen eigenen Wert von java.lang.SecurityManager zurückgegeben hat , der nicht die gleichen Überprüfungen wie der echte hatte.

/ * Versuche es aus unserem Repository zu laden * / classData = getClassImplFromDataBase (className); if (classData == null) {neue ClassNotFoundException () auslösen; }}

Nach den ersten Überprüfungen kommen wir zu dem obigen Code, in dem der einfache Klassenlader die Möglichkeit erhält, eine Implementierung dieser Klasse zu laden. Der SimpleClassLoader verfügt über eine Methode getClassImplFromDataBase (), die in unserem einfachen Beispiel dem Klassennamen lediglich das Verzeichnis "store \" voranstellt und die Erweiterung ".impl" anfügt. Ich habe diese Technik im Beispiel gewählt, damit es keine Frage gibt, ob der ursprüngliche Klassenlader unsere Klasse findet. Beachten Sie, dass der sun.applet.AppletClassLoader der Codebasis-URL auf der HTML-Seite, auf der ein Applet lebt, den Namen voranstellt und dann eine HTTP- Abrufanforderung zum Abrufen der Bytecodes ausführt.

 / * Definiere es (analysiere die Klassendatei) * / result = defineClass (classData, 0, classData.length); 

Wenn die Klassenimplementierung geladen wurde, besteht der vorletzte Schritt darin, die defineClass () -Methode aus java.lang.ClassLoader aufzurufen , die als erster Schritt der Klassenüberprüfung betrachtet werden kann. Diese Methode ist in der virtuellen Java-Maschine implementiert und überprüft, ob die Klassenbytes eine legale Java-Klassendatei sind. Intern füllt die defineClass- Methode eine Datenstruktur aus, die die JVM zum Halten von Klassen verwendet. Wenn die Klassendaten fehlerhaft sind, wird durch diesen Aufruf ein ClassFormatError ausgelöst.

if (resolveIt) {resolveClass (Ergebnis); }}

The last class loader-specific requirement is to call resolveClass() if the boolean parameter resolveIt was true. This method does two things: First, it causes any classes that are referenced by this class explicitly to be loaded and a prototype object for this class to be created; then, it invokes the verifier to do dynamic verification of the legitimacy of the bytecodes in this class. If verification fails, this method call will throw a LinkageError, the most common of which is a 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.

Das beste Beispiel für die erste Technik ist ein Webbrowser. Die von Java definierte Klasse, die von allen Applets implementiert wird, ist java.applet.Applet . Wenn eine Klasse von AppletClassLoader geladen wird , wird die erstellte Objektinstanz in eine Applet- Instanz umgewandelt . Wenn diese Umwandlung erfolgreich ist, wird die Methode init () aufgerufen. In meinem Beispiel verwende ich die zweite Technik, eine Schnittstelle.

Mit dem Beispiel spielen

Um das Beispiel abzurunden, habe ich noch ein paar erstellt

.Java

Dateien. Diese sind:

öffentliche Schnittstelle LocalModule {/ * Modul starten * / void start (String-Option); }}