Les bases des chargeurs de classes Java

Le concept de chargeur de classe, l'une des pierres angulaires de la machine virtuelle Java, décrit le comportement de conversion d'une classe nommée en bits responsables de l'implémentation de cette classe. Étant donné que les chargeurs de classe existent, l'environnement d'exécution Java n'a pas besoin de connaître les fichiers et les systèmes de fichiers lors de l'exécution de programmes Java.

Que font les chargeurs de classe

Les classes sont introduites dans l'environnement Java lorsqu'elles sont référencées par leur nom dans une classe déjà en cours d'exécution. Il y a un peu de magie qui continue pour faire fonctionner la première classe (c'est pourquoi vous devez déclarer la méthode main () comme statique, en prenant un tableau de chaînes comme argument), mais une fois que cette classe est en cours d'exécution, de futures tentatives de le chargement des classes est effectué par le chargeur de classe.

Dans sa forme la plus simple, un chargeur de classe crée un espace de nom plat de corps de classe référencés par un nom de chaîne. La définition de la méthode est:

Classe r = loadClass (String className, boolean resolutionIt); 

La variable className contient une chaîne comprise par le chargeur de classe et utilisée pour identifier de manière unique une implémentation de classe. La variable resolutionIt est un indicateur pour indiquer au chargeur de classe que les classes référencées par ce nom de classe doivent être résolues (c'est-à-dire que toute classe référencée doit également être chargée).

Toutes les machines virtuelles Java incluent un chargeur de classe intégré à la machine virtuelle. Ce chargeur intégré est appelé le chargeur de classe primordial. C'est quelque peu spécial car la machine virtuelle suppose qu'elle a accès à un référentiel de classes approuvées qui peut être exécuté par la machine virtuelle sans vérification.

Le chargeur de classe primordial implémente l'implémentation par défaut de loadClass () . Ainsi, ce code comprend que le nom de classe java.lang.Object est stocké dans un fichier avec le préfixe java / lang / Object.class quelque part dans le chemin de classe. Ce code implémente également la recherche de chemin de classe et la recherche dans les fichiers zip pour les classes. Ce qui est vraiment cool à propos de la façon dont cela est conçu, c'est que Java peut changer son modèle de stockage de classe simplement en modifiant l'ensemble des fonctions qui implémentent le chargeur de classe.

En creusant dans les tripes de la machine virtuelle Java, vous découvrirez que le chargeur de classe primordial est implémenté principalement dans les fonctions FindClassFromClass et ResolveClass .

Alors, quand les classes sont-elles chargées? Il y a exactement deux cas: lorsque le nouveau bytecode est exécuté (par exemple, FooClass f = new FooClass () ;) et lorsque les bytecodes font une référence statique à une classe (par exemple, System. Out ).

Un chargeur de classe non primordial

"Et alors?" vous pourriez demander.

La machine virtuelle Java a des hooks pour permettre à un chargeur de classe défini par l'utilisateur d'être utilisé à la place du chargeur primordial. De plus, étant donné que le chargeur de classe utilisateur obtient la première fissure au nom de la classe, l'utilisateur est capable d'implémenter n'importe quel nombre de référentiels de classe intéressants, notamment les serveurs HTTP - qui ont fait décoller Java en premier lieu.

Il y a un coût, cependant, parce que le chargeur de classe est si puissant (par exemple, il peut remplacer java.lang.Object par sa propre version), les classes Java comme les applets ne sont pas autorisées à instancier leurs propres chargeurs. (Ceci est imposé par le chargeur de classe, au fait.) Cette colonne ne sera pas utile si vous essayez de faire cela avec une applet, uniquement avec une application exécutée à partir du référentiel de classe approuvé (comme des fichiers locaux).

Un chargeur de classe utilisateur a la possibilité de charger une classe avant le chargeur de classe primordial. Pour cette raison, il peut charger les données d'implémentation de classe à partir d'une autre source, ce qui permet à AppletClassLoader de charger des classes à l'aide du protocole HTTP.

Construire un SimpleClassLoader

Un chargeur de classe commence par être une sous-classe de java.lang.ClassLoader . La seule méthode abstraite qui doit être implémentée est loadClass () . Le flux de loadClass () est le suivant:

  • Vérifiez le nom de la classe.
  • Vérifiez si la classe demandée a déjà été chargée.
  • Vérifiez si la classe est une classe "système".
  • Tentative de récupération de la classe dans le référentiel de ce chargeur de classe.
  • Définissez la classe de la machine virtuelle.
  • Résolvez la classe.
  • Renvoyez la classe à l'appelant.

SimpleClassLoader apparaît comme suit, avec des descriptions de ce qu'il fait entrecoupées du code.

Classe publique synchronisée loadClass (String className, boolean resolIt) jette ClassNotFoundException {Class result; byte classData []; System.out.println (">>>>>> Charger la classe:" + className); / * Consultez notre cache local de classes * / result = (Class) classes.get (className); if (result! = null) {System.out.println (">>>>>> renvoyant le résultat mis en cache."); résultat de retour; }

Le code ci-dessus est la première section de la méthode loadClass . Comme vous pouvez le voir, il prend un nom de classe et recherche une table de hachage locale que notre chargeur de classe gère les classes qu'il a déjà renvoyées. Il est important de conserver cette table de hachage car vous devez renvoyer la même référence d'objet de classe pour le même nom de classe à chaque fois qu'on vous le demande. Sinon, le système croira qu'il existe deux classes différentes avec le même nom et lèvera une ClassCastException chaque fois que vous attribuerez une référence d'objet entre elles. Il est également important de garder un cache car le loadClass () est appelée récursivement lorsqu'une classe est en cours de résolution, et vous devrez renvoyer le résultat mis en cache plutôt que de le rechercher pour une autre copie.

/ * Vérifier avec le chargeur de classe primordial * / try {result = super.findSystemClass (className); System.out.println (">>>>>> renvoyant la classe système (dans CLASSPATH)."); résultat de retour; } catch (ClassNotFoundException e) {System.out.println (">>>>>> Pas une classe système."); }

Comme vous pouvez le voir dans le code ci-dessus, l'étape suivante consiste à vérifier si le chargeur de classe primordial peut résoudre ce nom de classe. Cette vérification est essentielle à la fois pour l'intégrité et la sécurité du système. Par exemple, si vous renvoyez votre propre instance de java.lang.Object à l'appelant, alors cet objet ne partagera aucune superclasse commune avec aucun autre objet! La sécurité du système peut être compromise si votre chargeur de classe retournait sa propre valeur de java.lang.SecurityManager , qui n'avait pas les mêmes vérifications que le vrai.

/ * Essayez de le charger depuis notre référentiel * / classData = getClassImplFromDataBase (className); if (classData == null) {throw new ClassNotFoundException (); }

Après les vérifications initiales, nous arrivons au code ci-dessus, où le chargeur de classe simple a la possibilité de charger une implémentation de cette classe. Le SimpleClassLoader possède une méthode getClassImplFromDataBase () qui, dans notre exemple simple, préfixe simplement le répertoire "store \" au nom de la classe et ajoute l'extension ".impl". J'ai choisi cette technique dans l'exemple pour qu'il ne soit pas question que le chargeur de classe primordial trouve notre classe. Notez que sun.applet.AppletClassLoader préfixe l'URL de la base de code de la page HTML où se trouve une applet au nom, puis effectue une requête HTTP get pour récupérer les bytecodes.

 / * Define it (analyser le fichier de classe) * / result = defineClass (classData, 0, classData.length); 

Si l'implémentation de classe a été chargée, l'avant-dernière étape consiste à appeler la méthode defineClass () depuis java.lang.ClassLoader , ce qui peut être considéré comme la première étape de la vérification de classe. Cette méthode est implémentée dans la machine virtuelle Java et est chargée de vérifier que les octets de classe sont un fichier de classe Java légal. En interne, la méthode defineClass remplit une structure de données que la JVM utilise pour contenir des classes. Si les données de classe sont mal formées, cet appel provoquera la levée d' une ClassFormatError .

if (resolutionIt) {resolutionClass (résultat); }

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.

Le meilleur exemple de la première technique est un navigateur Web. La classe définie par Java qui est implémentée par toutes les applets est java.applet.Applet . Lorsqu'une classe est chargée par AppletClassLoader , l'instance d'objet créée est convertie en une instance d' Applet . Si cette conversion réussit, la méthode init () est appelée. Dans mon exemple, j'utilise la deuxième technique, une interface.

Jouer avec l'exemple

Pour compléter l'exemple, j'ai créé quelques autres

.Java

des dossiers. Ceux-ci sont:

interface publique LocalModule {/ * Démarre le module * / void start (option String); }