Le basi dei programmi di caricamento classi Java

Il concetto di class loader, uno dei capisaldi della Java virtual machine, descrive il comportamento di convertire una classe denominata nei bit responsabili dell'implementazione di quella classe. Poiché esistono programmi di caricamento classi, il runtime Java non ha bisogno di sapere nulla sui file e sui file system durante l'esecuzione dei programmi Java.

Cosa fanno i caricatori di classe

Le classi vengono introdotte nell'ambiente Java quando vengono referenziate in base al nome in una classe già in esecuzione. C'è un po 'di magia che va avanti per far funzionare la prima classe (motivo per cui devi dichiarare il metodo main () come statico, prendendo un array di stringhe come argomento), ma una volta che quella classe è in esecuzione, i futuri tentativi di le classi di caricamento vengono eseguite dal programma di caricamento classi.

Nella sua forma più semplice, un programma di caricamento classi crea uno spazio dei nomi semplice dei corpi delle classi a cui fa riferimento un nome di stringa. La definizione del metodo è:

Classe r = loadClass (String className, booleanubleshoIt); 

La variabile className contiene una stringa che viene compresa dal programma di caricamento classi e viene utilizzata per identificare in modo univoco un'implementazione di classe. La variabile resolutionIt è un flag per dire al programma di caricamento classi che le classi referenziate da questo nome classe dovrebbero essere risolte (cioè, dovrebbe essere caricata anche qualsiasi classe referenziata).

Tutte le Java virtual machine includono un class loader incorporato nella macchina virtuale. Questo caricatore incorporato è chiamato caricatore di classi primordiale. È un po 'speciale perché la macchina virtuale presume di avere accesso a un repository di classi affidabili che possono essere eseguite dalla VM senza verifica.

Il programma di caricamento classi primordiale implementa l'implementazione predefinita di loadClass () . Pertanto, questo codice comprende che il nome della classe java.lang.Object è memorizzato in un file con il prefisso java / lang / Object.class da qualche parte nel percorso della classe. Questo codice implementa anche la ricerca del percorso di classe e l'analisi dei file zip per le classi. La cosa veramente interessante del modo in cui è progettato è che Java può cambiare il suo modello di archiviazione di classe semplicemente cambiando l'insieme di funzioni che implementa il caricatore di classi.

Scavando nelle viscere della macchina virtuale Java, scoprirai che il caricatore di classi primordiale è implementato principalmente nelle funzioni FindClassFromClass e ResolveClass .

Quindi quando vengono caricate le classi? Ci sono esattamente due casi: quando viene eseguito il nuovo bytecode (ad esempio, FooClass f = new FooClass () ;) e quando i bytecode fanno un riferimento statico a una classe (ad esempio, System. Out ).

Un caricatore di classi non primordiale

"E allora?" potresti chiedere.

La Java virtual machine contiene degli hook per consentire l'utilizzo di un class loader definito dall'utente al posto di quello primordiale. Inoltre, poiché il programma di caricamento classi dell'utente ottiene il primo crack al nome della classe, l'utente è in grado di implementare un numero qualsiasi di archivi di classi interessanti, non ultimo dei quali sono i server HTTP, che hanno fatto decollare Java in primo luogo.

C'è un costo, tuttavia, poiché il class loader è così potente (ad esempio, può sostituire java.lang.Object con la sua versione), le classi Java come le applet non sono autorizzate a creare istanze dei propri caricatori. (Questo è imposto dal caricatore di classi, tra l'altro.) Questa colonna non sarà utile se stai cercando di fare queste cose con un'applet, solo con un'applicazione in esecuzione dal repository di classi attendibile (come i file locali).

Un programma di caricamento classi utente ha la possibilità di caricare una classe prima del programma di caricamento classi primordiale. Per questo motivo , può caricare i dati di implementazione della classe da un'origine alternativa, che è il modo in cui AppletClassLoader può caricare le classi utilizzando il protocollo HTTP.

Creazione di un SimpleClassLoader

Un programma di caricamento classi inizia con l'essere una sottoclasse di java.lang.ClassLoader . L'unico metodo astratto che deve essere implementato è loadClass () . Il flusso di loadClass () è il seguente:

  • Verifica il nome della classe.
  • Controlla se la classe richiesta è già stata caricata.
  • Controlla se la classe è una classe "di sistema".
  • Tenta di recuperare la classe dal repository di questo programma di caricamento classi.
  • Definisci la classe per la VM.
  • Risolvi la classe.
  • Restituisci la classe al chiamante.

SimpleClassLoader appare come segue, con le descrizioni di ciò che fa intervallate dal codice.

public synchronized Class loadClass (String className, booleanubleshoIt) genera ClassNotFoundException {Class result; byte classData []; System.out.println (">>>>>> Carica classe:" + className); / * Controlla la nostra cache locale delle classi * / result = (Class) classes.get (className); if (risultato! = null) {System.out.println (">>>>>> restituisce il risultato memorizzato nella cache."); risultato di ritorno; }

Il codice sopra è la prima sezione del metodo loadClass . Come puoi vedere, prende un nome di classe e cerca una tabella hash locale che il nostro programma di caricamento classi mantiene delle classi che ha già restituito. È importante mantenere questa tabella hash poiché è necessario restituire lo stesso riferimento all'oggetto di classe per lo stesso nome di classe ogni volta che viene richiesto. Altrimenti il ​​sistema crederà che ci siano due classi differenti con lo stesso nome e lancerà un'eccezione ClassCastException ogni volta che si assegna un riferimento a un oggetto tra di loro. È anche importante mantenere una cache perché loadClass () viene chiamato in modo ricorsivo quando una classe viene risolta e sarà necessario restituire il risultato memorizzato nella cache piuttosto che cercarlo per un'altra copia.

/ * Controlla con il caricatore di classi primordiale * / try {result = super.findSystemClass (className); System.out.println (">>>>>> restituisce la classe di sistema (in CLASSPATH)."); risultato di ritorno; } catch (ClassNotFoundException e) {System.out.println (">>>>>> Non è una classe di sistema."); }

Come puoi vedere nel codice sopra, il passaggio successivo è verificare se il caricatore di classi primordiale può risolvere questo nome di classe. Questo controllo è essenziale sia per la sanità mentale che per la sicurezza del sistema. Ad esempio, se restituisci la tua istanza di java.lang.Object al chiamante, questo oggetto non condividerà alcuna superclasse comune con nessun altro oggetto! La sicurezza del sistema può essere compromessa se il programma di caricamento classi restituisce il proprio valore java.lang.SecurityManager , che non ha gli stessi controlli di quello reale.

/ * Prova a caricarlo dal nostro repository * / classData = getClassImplFromDataBase (className); if (classData == null) {throw new ClassNotFoundException (); }

Dopo i controlli iniziali, arriviamo al codice sopra che è dove il semplice caricatore di classi ha l'opportunità di caricare un'implementazione di questa classe. Il SimpleClassLoader ha un metodo getClassImplFromDataBase () che nel nostro semplice esempio semplicemente antepone la directory "store \" al nome della classe e aggiunge l'estensione ".impl". Ho scelto questa tecnica nell'esempio in modo che non ci fossero dubbi sul caricamento della classe primordiale che trova la nostra classe. Notare che sun.applet.AppletClassLoader aggiunge come prefisso l'URL della base di codice dalla pagina HTML in cui si trova un'applet al nome e quindi esegue una richiesta HTTP get per recuperare i bytecode.

 / * Definiscilo (analizza il file di classe) * / result = defineClass (classData, 0, classData.length); 

Se l'implementazione della classe è stata caricata, il penultimo passaggio consiste nel chiamare il metodo defineClass () da java.lang.ClassLoader , che può essere considerato il primo passaggio della verifica della classe. Questo metodo è implementato nella macchina virtuale Java ed è responsabile della verifica che i byte della classe siano un file di classe Java legale. Internamente, il metodo defineClass compila una struttura dati che la JVM utilizza per contenere le classi. Se i dati della classe non sono corretti, questa chiamata causerà la generazione di un'eccezione ClassFormatError .

if (ubleshoit) {resolClass (risultato); }

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.

Il miglior esempio della prima tecnica è un browser Web. La classe definita da Java implementata da tutte le applet è java.applet.Applet . Quando una classe viene caricata da AppletClassLoader , viene eseguito il cast dell'istanza dell'oggetto creata a un'istanza di Applet . Se questo cast riesce , viene chiamato il metodo init () . Nel mio esempio utilizzo la seconda tecnica, un'interfaccia.

Giocando con l'esempio

Per completare l'esempio, ne ho creati altri due

.Giava

File. Questi sono:

interfaccia pubblica LocalModule {/ * Avvia il modulo * / void start (opzione String); }