Java ClassLoader

O Java ClassLoader é um dos componentes cruciais, mas raramente usados no desenvolvimento de projetos. Eu nunca estendi o ClassLoader em nenhum dos meus projetos. No entanto, a ideia de ter meu próprio ClassLoader que possa personalizar o carregamento de classes Java é emocionante. Este artigo fornecerá uma visão geral do Java ClassLoader e, em seguida, passará a criar um carregador de classes personalizado em Java.

O que é o Java ClassLoader?

Sabemos que o programa Java é executado na Máquina Virtual Java (JVM). Quando compilamos uma classe Java, a JVM cria o bytecode, que é independente da plataforma e da máquina. O bytecode é armazenado em um arquivo .class. Quando tentamos usar uma classe, o ClassLoader a carrega na memória.

Tipos de ClassLoader incorporados

Há três tipos de ClassLoader incorporados no Java.

  1. Bootstrap Class Loader – Carrega as classes internas do JDK. Ele carrega o rt.jar e outras classes principais, por exemplo, classes do pacote java.lang.*.
  2. Extensions Class Loader – Carrega classes do diretório de extensões do JDK, geralmente o diretório $JAVA_HOME/lib/ext.
  3. Carregador de Classes do Sistema – Este carregador de classes carrega classes do classpath atual. Podemos definir o classpath ao chamar um programa usando a opção de linha de comando -cp ou -classpath.

Hierarquia de ClassLoader

O ClassLoader é hierárquico no carregamento de uma classe na memória. Sempre que uma solicitação é feita para carregar uma classe, ela a delega para o carregador de classes pai. É assim que a singularidade é mantida no ambiente de tempo de execução. Se o carregador de classes pai não encontrar a classe, então o carregador de classes em si tenta carregar a classe. Vamos entender isso executando o programa Java abaixo.

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

    }

}

Saída:

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

Como o Carregador de Classes Java Funciona?

Vamos entender o funcionamento dos carregadores de classes a partir da saída do programa acima.

  • O Carregador de Classes java.util.HashMap está aparecendo como nulo, o que reflete o Bootstrap ClassLoader. O Carregador de Classes da classe DNSNameService é ExtClassLoader. Como a própria classe está no CLASSPATH, o System ClassLoader a carrega.
  • Quando estamos tentando carregar HashMap, nosso System ClassLoader delega isso para o Extension ClassLoader. O classloader de extensão delega isso para o Bootstrap ClassLoader. O carregador de classe de inicialização encontra a classe HashMap e a carrega na memória JVM.
  • O mesmo processo é seguido para a classe DNSNameService. Mas o Bootstrap ClassLoader não consegue localizá-la, pois está em $JAVA_HOME/lib/ext/dnsns.jar. Portanto, ela é carregada pelo Extensions Classloader.
  • A classe Blob está incluída no conector JDBC do MySql (mysql-connector-java-5.0.7-bin.jar), que está presente no caminho de construção do projeto. Também está sendo carregada pelo System Classloader.
  • As classes carregadas por um carregador de classes filho têm visibilidade nas classes carregadas por seus carregadores de classes pai. Portanto, as classes carregadas pelo System Classloader têm visibilidade nas classes carregadas pelos carregadores de classes Extensions e Bootstrap.
  • Se houver carregadores de classes irmãos, eles não poderão acessar classes carregadas uns pelos outros.

Por que escrever um Custom ClassLoader em Java?

O ClassLoader padrão do Java pode carregar classes do sistema de arquivos local, o que é suficientemente bom para a maioria dos casos. No entanto, se você espera uma classe em tempo de execução, de um servidor FTP ou por meio de um serviço da web de terceiros no momento do carregamento da classe, será necessário estender o carregador de classes existente. Por exemplo, os AppletViewers carregam classes de um servidor web remoto.

Métodos do Java ClassLoader

  • Quando a JVM solicita uma classe, ela invoca a função loadClass() do ClassLoader, passando o nome totalmente qualificado da classe.
  • A função loadClass() chama o método findLoadedClass() para verificar se a classe já foi carregada. Isso é necessário para evitar o carregamento da mesma classe várias vezes.
  • Se a classe ainda não estiver carregada, ela delegará a solicitação ao ClassLoader pai para carregar a classe.
  • Se o ClassLoader pai não encontrar a classe, ele invocará o método findClass() para procurar as classes no sistema de arquivos.

Exemplo de ClassLoader Personalizado em Java

Vamos criar nosso próprio ClassLoader estendendo a classe ClassLoader e substituindo o método loadClass(String name). Se o nome da classe começar com com.journaldev, carregaremos usando nosso carregador de classes personalizado; caso contrário, iremos invocar o método loadClass() do ClassLoader pai para carregar a classe.

1. CCLoader.java

Este é o nosso carregador de classes personalizado com os seguintes métodos.

  1. private byte[] loadClassFileData(String name): Este método lerá o arquivo de classe do sistema de arquivos para um array de bytes.
  2. private Class<?> getClass(String name): Este método chamará a função loadClassFileData() e, ao invocar o método defineClass() do pai, gerará a Classe e a retornará.
  3. public Class<?> loadClass(String name): Este método é responsável por carregar a classe. Se o nome da classe começar com com.journaldev (nossas classes de exemplo), ele a carregará usando o método getClass(); caso contrário, invocará a função loadClass() pai para carregá-la.
  4. public CCLoader(ClassLoader parent): Este é o construtor, que é responsável por definir o ClassLoader pai.
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 {
            // Este carrega os dados de byte code do arquivo
            b = loadClassFileData(file);
            // defineClass é herdado da classe ClassLoader
            // que converte uma matriz de bytes em uma Classe. defineClass é Final
            // então não podemos substituí-lo
            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 é a nossa classe de teste com a função principal. Estamos criando uma instância do nosso ClassLoader e carregando classes de exemplo usando seu método loadClass(). Após carregar a classe, estamos usando Java Reflection API para invocar seus 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);

        // O método abaixo é usado para verificar se Foo está sendo carregado
        // por nosso carregador de classes personalizado, ou seja, CCLoader
        Method printCL = clas.getMethod("printCL", null);
        printCL.invoke(null, new Object[0]);
    }
 
}

3. Foo.java e Bar.java

Estas são nossas classes de teste que estão sendo carregadas por nosso carregador de classes personalizado. Elas têm um método printCL(), que está sendo invocado para imprimir as informações do ClassLoader. A classe Foo será carregada pelo nosso carregador de classes personalizado. Foo usa a classe Bar, então a classe Bar também será carregada pelo nosso carregador de classes 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. Etapas de Execução do Carregador de Classes Personalizado Java

Primeiramente, vamos compilar todas as classes através da linha de comando. Depois disso, vamos executar a classe CCRun passando três argumentos. O primeiro argumento é o nome totalmente classificado para a classe Foo que será carregada pelo nosso carregador de classes. Os outros dois argumentos são passados para a função principal da classe Foo e para o construtor da classe Bar. As etapas de execução e a saída serão como abaixo.

$ 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
$

Se você observar a saída, está tentando carregar a classe com.journaldev.cl.Foo. Como ela está estendendo a classe java.lang.Object, está tentando carregar a classe Object primeiro. Então, a solicitação está chegando ao método loadClass de CCLoader, que está delegando para a classe pai. Assim, os carregadores de classes pai estão carregando as classes Object, String e outras classes java. Nosso ClassLoader está apenas carregando as classes Foo e Bar do sistema de arquivos. Isso fica claro a partir da saída da função printCL(). Podemos alterar a funcionalidade de loadClassFileData() para ler a matriz de bytes do servidor FTP ou invocando qualquer serviço de terceiros para obter a matriz de bytes da classe na hora. Espero que o artigo seja útil para entender o funcionamento do Java ClassLoader e como podemos estendê-lo para fazer muito mais do que apenas pegá-lo do sistema de arquivos.

Criando um Carregador de Classe Personalizado como Carregador de Classe Padrão

Podemos tornar nosso carregador de classes personalizado como o padrão quando a JVM inicia usando Opções Java. Por exemplo, eu vou executar o programa ClassLoaderTest mais uma vez após fornecer a opção java classloader.

$ 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
$

O CCLoader está carregando a classe ClassLoaderTest porque está no pacote com.journaldev.

Você pode baixar o código de exemplo do carregador de classes do nosso Repositório no GitHub.

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