Java ClassLoader

Le chargeur de classe Java est l’un des composants cruciaux mais rarement utilisés dans le développement de projets. Je n’ai jamais étendu ClassLoader dans aucun de mes projets. Mais l’idée d’avoir mon propre ClassLoader qui peut personnaliser le chargement des classes Java est excitante. Cet article donnera un aperçu du ClassLoader Java, puis passera à la création d’un chargeur de classe personnalisé en Java.

Qu’est-ce que le ClassLoader Java ?

Nous savons que les programmes Java s’exécutent sur la machine virtuelle Java (JVM). Lorsque nous compilons une classe Java, la JVM crée le bytecode, qui est indépendant de la plate-forme et de la machine. Le bytecode est stocké dans un fichier .class. Lorsque nous essayons d’utiliser une classe, le ClassLoader la charge en mémoire.

Types de ClassLoader intégrés

Il existe trois types de ClassLoader intégrés en Java.

  1. Bootstrap Class Loader – Il charge les classes internes du JDK. Il charge rt.jar et d’autres classes principales, par exemple les classes du package java.lang.*.
  2. Extensions Class Loader – Il charge les classes du répertoire des extensions du JDK, généralement le répertoire $JAVA_HOME/lib/ext.
  3. Chargeur de classe système – Ce chargeur de classe charge des classes depuis le chemin de classe actuel. Nous pouvons définir le chemin de classe en invoquant un programme à l’aide de l’option de ligne de commande -cp ou -classpath.

Hiérarchie des chargeurs de classe

Le chargeur de classe est hiérarchique dans le chargement d’une classe en mémoire. Chaque fois qu’une demande est faite pour charger une classe, elle est déléguée au chargeur de classe parent. C’est ainsi que l’unicité est maintenue dans l’environnement d’exécution. Si le chargeur de classe parent ne trouve pas la classe, le chargeur de classe lui-même tente de charger la classe. Comprenons cela en exécutant le programme Java ci-dessous.

package com.journaldev.classloader;

public class ClassLoaderTest {

    public static void main(String[] args) {

        System.out.println("class loader for HashMap: "
                + java.util.HashMap.class.getClassLoader());
        System.out.println("class loader for DNSNameService: "
                + sun.net.spi.nameservice.dns.DNSNameService.class
                        .getClassLoader());
        System.out.println("class loader for this class: "
                + ClassLoaderTest.class.getClassLoader());

        System.out.println(com.mysql.jdbc.Blob.class.getClassLoader());

    }

}

Résultat:

class loader for HashMap: null
class loader for DNSNameService: sun.misc.Launcher$ExtClassLoader@7c354093
class loader for this class: sun.misc.Launcher$AppClassLoader@64cbbe37
sun.misc.Launcher$AppClassLoader@64cbbe37

Comment fonctionne le chargeur de classe Java?

Comprendre le fonctionnement des chargeurs de classe à partir de la sortie du programme ci-dessus.

  • Le chargeur de classe java.util.HashMap apparaît comme nul, ce qui reflète le chargeur de classe Bootstrap. Le chargeur de classe de la classe DNSNameService est ExtClassLoader. Étant donné que la classe elle-même est dans le CLASSPATH, le chargeur de classe système la charge.
  • Lorsque nous essayons de charger HashMap, notre ClassLoader du système le délègue au ClassLoader d’extension. Le ClassLoader d’extension le délègue au ClassLoader Bootstrap. Le ClassLoader Bootstrap trouve la classe HashMap et la charge dans la mémoire JVM.
  • Le même processus est suivi pour la classe DNSNameService. Cependant, le ClassLoader Bootstrap ne parvient pas à la localiser car elle se trouve dans $JAVA_HOME/lib/ext/dnsns.jar. Par conséquent, elle est chargée par le ClassLoader des extensions.
  • La classe Blob est incluse dans le jar du connecteur MySQL JDBC (mysql-connector-java-5.0.7-bin.jar), qui est présent dans le chemin de construction du projet. Elle est également chargée par le ClassLoader du système.
  • Les classes chargées par un ClassLoader enfant ont une visibilité sur les classes chargées par ses ClassLoader parents. Ainsi, les classes chargées par le ClassLoader du système ont une visibilité sur les classes chargées par les ClassLoader des extensions et Bootstrap.
  • Si des ClassLoader frères existent, ils ne peuvent pas accéder aux classes chargées par les autres.

Pourquoi écrire un ClassLoader personnalisé en Java ?

Le chargeur de classes par défaut de Java peut charger des classes à partir du système de fichiers local, ce qui est suffisant dans la plupart des cas. Cependant, si vous attendez une classe au moment de l’exécution ou à partir du serveur FTP ou via un service Web tiers au moment du chargement de la classe, alors vous devez étendre le chargeur de classes existant. Par exemple, les visionneuses d’applets chargent les classes à partir d’un serveur Web distant.

Méthodes de Java ClassLoader

  • Lorsque la JVM demande une classe, elle invoque la fonction loadClass() du ClassLoader en passant le nom entièrement qualifié de la classe.
  • La fonction loadClass() appelle la méthode findLoadedClass() pour vérifier si la classe a déjà été chargée ou non. Il est nécessaire d’éviter de charger la même classe plusieurs fois.
  • Si la classe n’est pas déjà chargée, elle déléguera la demande au ClassLoader parent pour charger la classe.
  • Si le ClassLoader parent ne trouve pas la classe, il invoquera la méthode findClass() pour rechercher les classes dans le système de fichiers.

Exemple de chargeur de classes personnalisé Java

Nous allons créer notre propre ClassLoader en étendant la classe ClassLoader et en remplaçant la méthode loadClass(String name). Si le nom de la classe commence par com.journaldev, nous la chargerons en utilisant notre chargeur de classe personnalisé, sinon nous invoquerons la méthode loadClass() du ClassLoader parent pour charger la classe.

1. CCLoader.java

Voici notre chargeur de classe personnalisé avec les méthodes suivantes.

  1. private byte[] loadClassFileData(String name): Cette méthode lira le fichier de classe du système de fichiers dans un tableau d’octets.
  2. private Class<?> getClass(String name): Cette méthode appellera la fonction loadClassFileData() et en invoquant la méthode defineClass() parent, elle générera la Classe et la renverra.
  3. public Class<?> loadClass(String name): Cette méthode est responsable du chargement de la classe. Si le nom de la classe commence par com.journaldev (nos classes d’exemple), elle la chargera en utilisant la méthode getClass(), sinon elle invoquera la fonction loadClass() parent pour la charger.
  4. public CCLoader(ClassLoader parent): C’est le constructeur, qui est responsable de la définition du ClassLoader parent.
import java.io.DataInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
 
/**
 * Our Custom ClassLoader to load the classes. Any class in the com.journaldev
 * package will be loaded using this ClassLoader. For other classes, it will delegate the request to its Parent ClassLoader.
 *
 */
public class CCLoader extends ClassLoader {
 
    /**
     * This constructor is used to set the parent ClassLoader
     */
    public CCLoader(ClassLoader parent) {
        super(parent);
    }
 
    /**
     * Loads the class from the file system. The class file should be located in
     * the file system. The name should be relative to get the file location
     *
     * @param name
     *            Fully Classified name of the class, for example, com.journaldev.Foo
     */
    private Class getClass(String name) throws ClassNotFoundException {
        String file = name.replace('.', File.separatorChar) + ".class";
        byte[] b = null;
        try {
            // Ceci charge les données du bytecode à partir du fichier
            b = loadClassFileData(file);
            // defineClass est héritée de la classe ClassLoader
            // qui convertit un tableau de bytes en une classe. defineClass est final
            // donc nous ne pouvons pas la redéfinir
            Class c = defineClass(name, b, 0, b.length);
            resolveClass(c);
            return c;
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }
 
    /**
     * Every request for a class passes through this method. If the class is in
     * com.journaldev package, we will use this classloader or else delegate the
     * request to parent classloader.
     *
     *
     * @param name
     *            Full class name
     */
    @Override
    public Class loadClass(String name) throws ClassNotFoundException {
        System.out.println("Loading Class '" + name + "'");
        if (name.startsWith("com.journaldev")) {
            System.out.println("Loading Class using CCLoader");
            return getClass(name);
        }
        return super.loadClass(name);
    }
 
    /**
     * Reads the file (.class) into a byte array. The file should be
     * accessible as a resource and make sure that it's not in Classpath to avoid
     * any confusion.
     *
     * @param name
     *            Filename
     * @return Byte array read from the file
     * @throws IOException
     *             if an exception comes in reading the file
     */
    private byte[] loadClassFileData(String name) throws IOException {
        InputStream stream = getClass().getClassLoader().getResourceAsStream(
                name);
        int size = stream.available();
        byte buff[] = new byte[size];
        DataInputStream in = new DataInputStream(stream);
        in.readFully(buff);
        in.close();
        return buff;
    }
}

2. CCRun.java

Ceci est notre classe de test avec la fonction main. Nous créons une instance de notre ClassLoader et chargeons des classes d’exemple à l’aide de sa méthode loadClass(). Après avoir chargé la classe, nous utilisons l’API de réflexion Java pour invoquer ses méthodes.

import java.lang.reflect.Method;
 
public class CCRun {
 
    public static void main(String args[]) throws Exception {
        String progClass = args[0];
        String progArgs[] = new String[args.length - 1];
        System.arraycopy(args, 1, progArgs, 0, progArgs.length);

        CCLoader ccl = new CCLoader(CCRun.class.getClassLoader());
        Class clas = ccl.loadClass(progClass);
        Class mainArgType[] = { (new String[0]).getClass() };
        Method main = clas.getMethod("main", mainArgType);
        Object argsArray[] = { progArgs };
        main.invoke(null, argsArray);

        // La méthode ci-dessous est utilisée pour vérifier que Foo est chargée
        // par notre chargeur de classes personnalisé, c'est-à-dire CCLoader
        Method printCL = clas.getMethod("printCL", null);
        printCL.invoke(null, new Object[0]);
    }
 
}

3. Foo.java et Bar.java

Ce sont nos classes de test qui sont chargées par notre chargeur de classes personnalisé. Elles ont une méthode printCL() qui est invoquée pour afficher les informations du ClassLoader. La classe Foo sera chargée par notre chargeur de classes personnalisé. Foo utilise la classe Bar, donc la classe Bar sera également chargée par notre chargeur de classes personnalisé.

package com.journaldev.cl;
 
public class Foo {
    static public void main(String args[]) throws Exception {
        System.out.println("Foo Constructor >>> " + args[0] + " " + args[1]);
        Bar bar = new Bar(args[0], args[1]);
        bar.printCL();
    }
 
    public static void printCL() {
        System.out.println("Foo ClassLoader: "+Foo.class.getClassLoader());
    }
}
package com.journaldev.cl;
 
public class Bar {
 
    public Bar(String a, String b) {
        System.out.println("Bar Constructor >>> " + a + " " + b);
    }
 
    public void printCL() {
        System.out.println("Bar ClassLoader: "+Bar.class.getClassLoader());
    }
}

4. Étapes d’exécution du Custom ClassLoader Java

Tout d’abord, nous compilerons toutes les classes via la ligne de commande. Ensuite, nous exécuterons la classe CCRun en passant trois arguments. Le premier argument est le nom entièrement classifié pour la classe Foo qui sera chargée par notre chargeur de classe. Les deux autres arguments sont passés à la fonction principale de la classe Foo et au constructeur de Bar. Les étapes d’exécution et la sortie seront comme ci-dessous.

$ javac -cp . com/journaldev/cl/Foo.java
$ javac -cp . com/journaldev/cl/Bar.java
$ javac CCLoader.java
$ javac CCRun.java
CCRun.java:18: warning: non-varargs call of varargs method with inexact argument type for last parameter;
cast to java.lang.Class<?> for a varargs call
cast to java.lang.Class<?>[] for a non-varargs call and to suppress this warning
Method printCL = clas.getMethod("printCL", null);
^
1 warning
$ java CCRun com.journaldev.cl.Foo 1212 1313
Loading Class 'com.journaldev.cl.Foo'
Loading Class using CCLoader
Loading Class 'java.lang.Object'
Loading Class 'java.lang.String'
Loading Class 'java.lang.Exception'
Loading Class 'java.lang.System'
Loading Class 'java.lang.StringBuilder'
Loading Class 'java.io.PrintStream'
Foo Constructor >>> 1212 1313
Loading Class 'com.journaldev.cl.Bar'
Loading Class using CCLoader
Bar Constructor >>> 1212 1313
Loading Class 'java.lang.Class'
Bar ClassLoader: CCLoader@71f6f0bf
Foo ClassLoader: CCLoader@71f6f0bf
$

Si vous regardez la sortie, elle essaie de charger la classe com.journaldev.cl.Foo. Comme elle étend la classe java.lang.Object, elle essaie d’abord de charger la classe Object. Ainsi, la demande arrive à la méthode loadClass de CCLoader, qui la délègue à la classe parente. Ainsi, les chargeurs de classes parentes chargent les classes Object, String et autres classes java. Notre chargeur de classes charge uniquement les classes Foo et Bar à partir du système de fichiers. C’est clair à partir de la sortie de la fonction printCL(). Nous pouvons modifier la fonctionnalité loadClassFileData() pour lire le tableau d’octets à partir du serveur FTP ou en invoquant un service tiers pour obtenir le tableau d’octets de la classe à la volée. J’espère que l’article sera utile pour comprendre le fonctionnement du Java ClassLoader et comment nous pouvons l’étendre pour faire beaucoup plus que simplement le prendre à partir du système de fichiers.

Création d’un Custom ClassLoader comme ClassLoader par Défaut

Nous pouvons faire de notre chargeur de classes personnalisé celui par défaut lorsque la JVM démarre en utilisant les options Java. Par exemple, je vais exécuter à nouveau le programme ClassLoaderTest après avoir fourni l’option de chargeur de classes Java.

$ javac -cp .:../lib/mysql-connector-java-5.0.7-bin.jar com/journaldev/classloader/ClassLoaderTest.java
$ java -cp .:../lib/mysql-connector-java-5.0.7-bin.jar -Djava.system.class.loader=CCLoader com.journaldev.classloader.ClassLoaderTest
Loading Class 'com.journaldev.classloader.ClassLoaderTest'
Loading Class using CCLoader
Loading Class 'java.lang.Object'
Loading Class 'java.lang.String'
Loading Class 'java.lang.System'
Loading Class 'java.lang.StringBuilder'
Loading Class 'java.util.HashMap'
Loading Class 'java.lang.Class'
Loading Class 'java.io.PrintStream'
class loader for HashMap: null
Loading Class 'sun.net.spi.nameservice.dns.DNSNameService'
class loader for DNSNameService: sun.misc.Launcher$ExtClassLoader@24480457
class loader for this class: CCLoader@38503429
Loading Class 'com.mysql.jdbc.Blob'
sun.misc.Launcher$AppClassLoader@2f94ca6c
$

Le CCLoader charge la classe ClassLoaderTest car elle se trouve dans le package com.journaldev.

Vous pouvez télécharger le code d’exemple du chargeur de classes depuis notre Dépôt GitHub.

Source:
https://www.digitalocean.com/community/tutorials/java-classloader