Java ClassLoader

Java ClassLoader es uno de los componentes cruciales pero raramente utilizados en el desarrollo de proyectos. Nunca he extendido ClassLoader en ninguno de mis proyectos. Sin embargo, la idea de tener mi propio ClassLoader que pueda personalizar la carga de clases de Java es emocionante. Este artículo proporcionará una visión general de Java ClassLoader y luego avanzará para crear un cargador de clases personalizado en Java.

¿Qué es Java ClassLoader?

Sabemos que el programa Java se ejecuta en la Máquina Virtual Java (JVM). Cuando compilamos una clase Java, la JVM crea el bytecode, que es independiente de la plataforma y la máquina. El bytecode se almacena en un archivo .class. Cuando intentamos usar una clase, el ClassLoader la carga en la memoria.

Tipos de ClassLoader Integrados

Existen tres tipos de ClassLoader integrados en Java.

  1. Bootstrap Class Loader: carga clases internas de JDK. Carga rt.jar y otras clases centrales, por ejemplo, las clases del paquete java.lang.*.
  2. Extensions Class Loader: carga clases desde el directorio de extensiones JDK, generalmente el directorio $JAVA_HOME/lib/ext.
  3. Clase del Cargador del Sistema – Este cargador de clases carga clases desde la ruta de clases actual. Podemos establecer la ruta de clases mientras invocamos un programa usando la opción de línea de comando -cp o -classpath.

Jerarquía del Cargador de Clases

El cargador de clases es jerárquico en la carga de una clase en memoria. Cada vez que se genera una solicitud para cargar una clase, se delega al cargador de clases padre. Así es como se mantiene la singularidad en el entorno de tiempo de ejecución. Si el cargador de clases padre no encuentra la clase, entonces el propio cargador de clases intenta cargar la clase. Entendamos esto ejecutando el siguiente programa en Java.

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());

    }

}

Salida:

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

¿Cómo funciona el Cargador de Clases en Java?

Entendamos el funcionamiento de los cargadores de clases a partir de la salida del programa anterior.

  • El Cargador de Clases de la clase java.util.HashMap aparece como nulo, lo que refleja el Cargador de Clases de Inicio (Bootstrap ClassLoader). El Cargador de Clases de la clase DNSNameService es ExtClassLoader. Dado que la clase en sí está en la RUTA DE CLASES, el Cargador de Clases del Sistema la carga.
  • Cuando intentamos cargar HashMap, nuestro ClassLoader del Sistema lo delega al ClassLoader de Extensión. El ClassLoader de extensión lo delega al Bootstrap ClassLoader. El bootstrap class loader encuentra la clase HashMap y la carga en la memoria de la JVM.
  • El mismo proceso se sigue para la clase DNSNameService. Sin embargo, el Bootstrap ClassLoader no puede localizarla ya que está en $JAVA_HOME/lib/ext/dnsns.jar. Por lo tanto, se carga mediante Extensions Classloader.
  • La clase Blob está incluida en el conector jar de MySql JDBC (mysql-connector-java-5.0.7-bin.jar), que está presente en la ruta de compilación del proyecto. También se carga mediante el System Classloader.
  • Las clases cargadas por un cargador de clases hijo tienen visibilidad en las clases cargadas por sus cargadores de clases padre. Entonces, las clases cargadas por el System Classloader tienen visibilidad en las clases cargadas por los Classloaders de Extensiones y de Bootstrap.
  • Si hay cargadores de clases hermanos, entonces no pueden acceder a las clases cargadas por los demás.

¿Por qué escribir un ClassLoader personalizado en Java?

El ClassLoader predeterminado de Java puede cargar clases desde el sistema de archivos local, lo cual es suficientemente bueno para la mayoría de los casos. Pero, si esperas una clase en tiempo de ejecución o desde el servidor FTP o a través de un servicio web de terceros en el momento de cargar la clase, entonces debes extender el ClassLoader existente. Por ejemplo, los AppletViewers cargan las clases desde un servidor web remoto.

Métodos de Java ClassLoader

  • Cuando la JVM solicita una clase, invoca la función loadClass() del ClassLoader pasando el nombre completamente clasificado de la Clase.
  • La función loadClass() llama al método findLoadedClass() para verificar si la clase ya ha sido cargada o no. Es necesario para evitar cargar la misma clase varias veces.
  • Si la Clase aún no ha sido cargada, entonces delegará la solicitud al ClassLoader padre para cargar la clase.
  • Si el ClassLoader padre no encuentra la clase, entonces invocará el método findClass() para buscar las clases en el sistema de archivos.

Ejemplo de ClassLoader Personalizado en Java

Crearemos nuestro propio ClassLoader extendiendo la clase ClassLoader y sobrescribiendo el método loadClass(String name). Si el nombre de la clase comienza con com.journaldev, la cargaremos utilizando nuestro cargador de clases personalizado; de lo contrario, invocaremos el método loadClass() del ClassLoader padre para cargar la clase.

1. CCLoader.java

Este es nuestro cargador de clases personalizado con los siguientes métodos.

  1. private byte[] loadClassFileData(String name): Este método leerá el archivo de clase del sistema de archivos a una matriz de bytes.
  2. private Class<?> getClass(String name): Este método llamará a la función loadClassFileData() e invocando el método defineClass() del padre, generará la Clase y la devolverá.
  3. public Class<?> loadClass(String name): Este método es responsable de cargar la clase. Si el nombre de la clase comienza con com.journaldev (nuestras clases de ejemplo), la cargará utilizando el método getClass(), de lo contrario, invocará la función loadClass() del padre para cargarla.
  4. public CCLoader(ClassLoader parent): Este es el constructor, que es responsable de establecer el ClassLoader padre.
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 {
            // Esto carga los datos del código de bytes desde el archivo
            b = loadClassFileData(file);
            // defineClass se hereda de la clase ClassLoader
            // que convierte una matriz de bytes en una Clase. defineClass es Final
            // así que no podemos anularlo
            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

Esta es nuestra clase de prueba con la función principal. Estamos creando una instancia de nuestro ClassLoader y cargando clases de muestra usando su método loadClass(). Después de cargar la clase, estamos utilizando la API de Reflexión de Java para invocar sus métodos.

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);

        // El método siguiente se utiliza para verificar que Foo se está cargando
        // por nuestro cargador de clases personalizado, es decir, CCLoader
        Method printCL = clas.getMethod("printCL", null);
        printCL.invoke(null, new Object[0]);
    }
 
}

3. Foo.java y Bar.java

Estas son nuestras clases de prueba que se cargan mediante nuestro cargador de clases personalizado. Tienen un método printCL(), que se invoca para imprimir la información del ClassLoader. La clase Foo será cargada por nuestro cargador de clases personalizado. Foo utiliza la clase Bar, por lo que la clase Bar también será cargada por nuestro cargador de clases personalizado.

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. Pasos de Ejecución del ClassLoader Personalizado de Java

En primer lugar, compilaremos todas las clases a través de la línea de comandos. Después de eso, ejecutaremos la clase CCRun pasando tres argumentos. El primer argumento es el nombre completamente clasificado para la clase Foo que será cargada por nuestro cargador de clases. Los otros dos argumentos se pasan a la función principal de la clase Foo y al constructor de la clase Bar. Los pasos de ejecución y la salida serán como se muestra a continuación.

$ 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 observas la salida, está intentando cargar la clase com.journaldev.cl.Foo. Dado que está extendiendo la clase java.lang.Object, está intentando cargar primero la clase Object. Así que la solicitud llega al método loadClass de CCLoader, que la está delegando a la clase principal. Entonces, los cargadores de clases principales están cargando las clases Object, String y otras clases java. Nuestro ClassLoader solo carga las clases Foo y Bar desde el sistema de archivos. Esto queda claro en la salida de la función printCL(). Podemos cambiar la funcionalidad de loadClassFileData() para leer la matriz de bytes desde un servidor FTP o invocar cualquier servicio de terceros para obtener la matriz de bytes de la clase sobre la marcha. Espero que el artículo sea útil para comprender el funcionamiento del ClassLoader de Java y cómo podemos ampliarlo para hacer mucho más que simplemente tomarlo del sistema de archivos.

Haciendo un ClassLoader Personalizado como el ClassLoader Predeterminado

Podemos hacer nuestro cargador de clases personalizado como el predeterminado cuando JVM se inicia usando opciones de Java. Por ejemplo, ejecutaré el programa ClassLoaderTest una vez más después de proporcionar la opción del cargador de clases de 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
$

El CCLoader está cargando la clase ClassLoaderTest porque está en el paquete com.journaldev.

Puedes descargar el código de ejemplo del cargador de clases desde nuestro Repositorio de GitHub.

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