Neste artigo iremos explicar o funcionamento do ClassLoader em Java, um dos principais recursos que, sem ele, não seria possível o uso de suas Classes e sua aplicação não funcionaria.

Mas afinal, o que é o ClassLoader? É uma Classe que carrega outras Classes; de forma mais científica, significa que ele carrega os bytecodes da sua classe para a memória, assim ela poderá ser utilizada por toda a sua aplicação. O ClassLoader encontra-se no pacote java.lang.ClassLoader.

Porque é necessário o ClassLoader? Este mecanismo de carregamento de classes nos permitir ter várias classes com o mesmo nome (exatamente iguais), mas em ClassLoaders diferentes. É isso que acontece, por exemplo, em containers web atuais, onde temos várias aplicações distintas, mas pode ocorrer que duas ou mais classes tenham o mesmo nome, isso não causará conflito algum.

Então você pode ser perguntar: Se tudo é carregado por um ClassLoader e o próprio ClassLoader é uma Classe que também deve ser carregada, então quem carrega o primeiro ClassLoader que carregará as demais classes? Pensando nesse problema foi criado o “Bootstrap ClassLoader” escrito em linguagem nativa, que é carregado antes de qualquer outro na JVM. Ele é o responsável por inicializar pacotes essenciais para funcionamento da linguagem, como o pacote “rt.jar”.

Então, se as Classes java são carregadas dinamicamente (através do ClassLoader), eu posso substituir o código de uma classe e recarregá-la em tempo de execução? Sim pode. E vai além disso: você ainda pode carregar uma classe remota através de uma URL qualquer em tempo de execução.

Exemplificando

Antes de entrarmos no mérito de explicar o funcionamento de cada ClassLoader nativo do Java, temos que entender o porque de seu uso se faz tão necessário.

O ClassLoader, por padrão, carrega todas as classes que estão presentes no seu CLASSPATH, então neste caso você não precisa nem saber da existência desse mecanismo de loading. Classes são identificadas através do seu fully qualified name + class loader que a carregou, assim temos a certeza de que uma Classe Funcionario da Aplicação 001 é diferente da Classe Funcionario da Aplicação 002.

A utilização dos ClassLoaders é útil, por exemplo, quando trabalhamos com várias aplicações com versões de bibliotecas diferentes. Ex: A aplicação 001 pode trabalhar com a versão 3.6 do Hibernate, enquanto a aplicação 002 trabalha com a versão 3.3. Se estivessem trabalhando no mesmo ClassLoader teríamos conflitos em classes como org.hibernate.Session, por estarem carregadas duas vezes na memória. Com classloaders distintos podemos carregar a org.hibernate.Session da versão 3.3 em um classloader e outro org.hibernate.Session do hibernate 3.6 em um outro classloader. Observe a Listagem 1.

Listagem 1. Carregando Classe


  Class r = loadClass(String className, boolean resolveIt);
  

Temos acima o carregamento/loading de uma classe na memória, onde o parâmetro “className” corresponde ao nome absoluto da classe, ou seja, o nome completo com o caminho do pacote (br.com.meupacote.MinhaClass). O parâmetro booleano “resolveIt” diz se as classes associadas a nossa classe carregada também devem ser carregadas, como se fosse um “cascade” de loadings.

Mecanimos de ClassLoader

Temos três tipos de ClassLoaders nativos: Bootstrap ClassLoader, Extension ClassLoader e Application ClassLoader. Cada um destes tem suas respectivas funções:

  • Bootstrap ClassLoader: Este é responsável por carregar as classes do rt.jar e não possui nenhum parent, ou seja, este é pai de todos os outros. Sendo assim, asseguramos que ninguém vai modificar classes do rt.jar, como do pacote java.lang, por exemplo.
  • Extension ClassLoader: Este é responsável por carregar os JARs que estão dentro do diretório da propriedade java.ext.dirs. Por padrão, esse diretório é: $JAVA_HOME/lib/ext. O pai desse ClassLoader é o Bootstrap ClassLoader.
  • Application ClassLoader: Este é responsável por carregar todas as classes da sua aplicação, ou seja, carrega tudo definido do seu CLASSPATH. O pai dele é o Extension ClassLoader.

Podemos dar um exemplo mais completo, carregando uma classe dinamicamente através do loadClass. Para isso, observe a Listagem 2.

Listagem 2. Carregando Classe – Completo


  public class Principal extends ClassLoader {
   
   public static void main(String[] args){
       ClassLoader classLoader = Principal.class.getClassLoader();
   
       try {
          Class aClass = classLoader.loadClass("AnotherClass");
          System.out.println("aClass.getName() = " + aClass.getName());
      } catch (ClassNotFoundException e) {
          e.printStackTrace();
      }
    }
  }

A saída do código acima será algo como: “aClass.getName() = AnotherClass”.

Vamos agora criar nosso próprio ClassLoader, que tem como principal método o “loadClass”, que é responsável por carregar a Classe que desejamos. Observe a Listagem 3.

Listagem 3. Criando nosso próprio ClassLoader


  import java.io.ByteArrayOutputStream;
  import java.io.IOException;
  import java.io.InputStream;
  import java.net.MalformedURLException;
  import java.net.URL;
  import java.net.URLConnection;
   
  public class MyClassLoader extends ClassLoader{
   
      public MyClassLoader(ClassLoader parent) {
          super(parent);
      }
   
      public Class loadClass(String name) throws ClassNotFoundException {
          if(!"reflection.MyObject".equals(name))
                  return super.loadClass(name);
   
          try {
              String url = "file:C:/data/projects/tutorials/web/WEB-INF/" +
                              "classes/reflection/MyObject.class";
              URL myUrl = new URL(url);
              URLConnection connection = myUrl.openConnection();
              InputStream input = connection.getInputStream();
              ByteArrayOutputStream buffer = new ByteArrayOutputStream();
              int data = input.read();
   
              while(data != -1){
                  buffer.write(data);
                  data = input.read();
              }
   
              input.close();
   
              byte[] classData = buffer.toByteArray();
   
              return defineClass("reflection.MyObject",
                      classData, 0, classData.length);
   
          } catch (MalformedURLException e) {
              e.printStackTrace();
          } catch (IOException e) {
              e.printStackTrace();
          }
   
          return null;
      }
   
  }

No código acima vemos que se a classe carregada estiver dentro do pacote reflection e tiver o nome MyObject, utilizaremos uma URL própria para carregar essa classe, caso contrário, passaremos a responsabilidade para o ClassLoader parent. Vamos agora criar um método main que utilizará nosso ClassLoader, conforme a Listagem 4.

Listagem 4. Utilizando o nosso ClassLoader


  public class Principal extends ClassLoader {
          public static void main(String[] args) throws
           ClassNotFoundException,
           IllegalAccessException,
           InstantiationException {
   
            ClassLoader parentClassLoader = MyClassLoader.class.getClassLoader();
            MyClassLoader classLoader = new MyClassLoader(parentClassLoader);
            Class myObjectClass = classLoader.loadClass("reflection.MyObject");
            AnInterface2       object1 =
             (AnInterface2) myObjectClass.newInstance();
   
            MyObjectSuperClass object2 =
              (MyObjectSuperClass) myObjectClass.newInstance();
   
            //create new class loader so classes can be reloaded.
           classLoader = new MyClassLoader(parentClassLoader);
           myObjectClass = classLoader.loadClass("reflection.MyObject");
   
           object1 = (AnInterface2)       myObjectClass.newInstance();
           object2 = (MyObjectSuperClass) myObjectClass.newInstance();
       }
  }

Vamos entender o funcionamento do código acima: primeiramente capturamos o ClassLoader do nosso ClassLoader, ou seja, o parent do nosso ClassLoader (MyClassLoader). Então iremos criar uma instância do nosso ClassLoader passando como parâmetro o parent dele, assim ele poderá delegar a função de loadClass para o parent caso necessite.

Carregamos a classe “reflection.MyObject” no nosso novo ClassLoader (lembre que nessa hora ele irá carregar a classe de uma URL específica, que é diferente do nosso CLASSPATH). Com a class carregada em um classLoader próprio nosso, podemos agora recarregar a mesma classe e fazer um reloading, fazendo novamente um “loadClass”.

As vezes que você irá precisar criar seu próprio ClassLoader serão raras, na verdade, você pode nunca precisar trabalhar com ele diretamente, mas a sua utilidade como um todo é essencial e o conhecimento deste é um diferencial com certeza. Você poderá resolver problemas como “NoClassDefFoundError” em pouco tempo, apenas conhecendo o princípio básico de como funciona o ClassLoader, pois nestes casos a classe pode existir fisicamente, mas em outro ClassLoader que não é o que sua aplicação está utilizando.