Carregador de Classes Java

Brazilian Portuguese Translation:
Java ClassLoader é um dos componentes cruciais, mas raramente utilizados 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 pode personalizar o carregamento de classes Java é empolgante. Este artigo fornecerá uma visão geral do Java ClassLoader e depois avançará para 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 de plataforma e máquina. O bytecode é armazenado em um arquivo .class. Quando tentamos usar uma classe, o ClassLoader a carrega na memória.

Tipos de ClassLoader Integrados

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

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

Hierarquia do Carregador de Classe

O Carregador de Classe é hierárquico ao carregar uma classe na memória. Sempre que uma solicitação é feita para carregar uma classe, ela é delegada para o carregador de classes pai. É assim que a singularidade é mantida no ambiente de execução. Se o carregador de classes pai não encontrar a classe, o próprio carregador de classes 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());

    }

}

Resultado:

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 Classe do Java Funciona?

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

  • O Carregador de Classe java.util.HashMap está chegando como nulo, o que reflete o Carregador de Classe Bootstrap. O Carregador de Classe da classe DNSNameService é ExtClassLoader. Como a própria classe está no CLASSPATH, o Carregador de Classe do Sistema a carrega.
  • Quando estamos tentando carregar o HashMap, nosso System ClassLoader delega isso para o Extension ClassLoader. O extension class loader, por sua vez, delega para o Bootstrap ClassLoader. O bootstrap class loader encontra a classe HashMap e a carrega na memória da JVM.
  • O mesmo processo é seguido para a classe DNSNameService. No entanto, 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 arquivo jar do conector MySql JDBC (mysql-connector-java-5.0.7-bin.jar), que está presente no caminho de compilação do projeto. Ela também é carregada pelo System Classloader.
  • As classes carregadas por um class loader filho têm visibilidade das classes carregadas por seus class loaders pai. Portanto, as classes carregadas pelo System Classloader têm visibilidade das classes carregadas pelos class loaders Extensions e Bootstrap.
  • Se houver class loaders irmãos, eles não podem acessar as classes carregadas um pelo outro.

Por que escrever um Custom ClassLoader em Java?

O ClassLoader padrão do Java pode carregar classes do sistema de arquivos local, o que é suficiente para a maioria dos casos. Mas, se você estiver esperando por uma classe em tempo de execução ou do servidor FTP ou através de um serviço da web de terceiros no momento do carregamento da classe, então você terá que estender o carregador de classes existente. Por exemplo, os AppletViewers carregam as classes de um servidor web remoto.

Métodos do ClassLoader do Java

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

Exemplo de ClassLoader Personalizado do 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, então carregaremos usando nosso carregador de classe personalizado, caso contrário, invocaremos o método loadClass() do ClassLoader pai para carregar a classe.

1. CCLoader.java

Este é o nosso carregador de classe 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 as carregará usando o método getClass(), caso contrário, invocará a função loadClass() do 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 {
            // Isso carrega os dados do código de bytes 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 é 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(). Depois de carregar a classe, estamos usando a API de Reflexão do Java 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 o Foo está sendo carregado
        // pelo 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 pelo 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. Passos de Execução do Java Custom ClassLoader

Primeiramente, compilaremos todas as classes através da linha de comando. Em seguida, executaremos 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 o construtor da classe Bar. Os passos de execução e a saída serão como segue.

$ 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. Assim, a solicitação chega ao método loadClass de CCLoader, que a delega para a classe pai. Portanto, os carregadores de classes pai estão carregando as classes Object, String e outras classes java. Nosso ClassLoader está carregando apenas 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 do método loadClassFileData() para ler a matriz de bytes de um servidor FTP ou invocar qualquer serviço de terceiros para obter a matriz de bytes da classe dinamicamente. Espero que o artigo seja útil para entender o funcionamento do Java ClassLoader e como podemos estendê-lo para fazer muito mais do que simplesmente obtê-lo do sistema de arquivos.

Fazendo um Carregador de Classes Personalizado como Carregador Padrão

Podemos tornar nosso carregador de classes personalizado como o padrão quando a JVM inicia usando as Opções Java. Por exemplo, vou executar o programa ClassLoaderTest mais uma vez depois de fornecer a opção do carregador 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
$

O CCLoader está carregando a classe ClassLoaderTest porque ela 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