ClassLoader é uma classe que carrega outras classes ou de forma mais científica, é a que se encontra no pacote java.lang.ClassLoader, carregando os bytecodes da sua classe para a memória. Assim, ela poderá ser utilizada por toda sua aplicação. Esse mecanismo de carregamento de classes nos permite ter várias delas 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 de uma ou duas classes terem o mesmo nome, sem causar conflito algum.

Tudo em Java é carregado através de ClassLoaders específicos (que serão explicados mais a frente). Então você pode se perguntar: se tudo é carregado por um ClassLoader e o próprio é 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 e que é carregado antes de qualquer outro na JVM, ficando responsável por inicializar pacotes essenciais para o funcionamento da linguagem, como o pacote “rt.jar”.

Então se as classes Java são carregadas dinamicamente (através do ClassLoader) podemos substituir o código de uma classe e recarregá-la em tempo de execução? Sim podemos. Na verdade, vai além disso, pois ainda podemos carregar uma classe remota através de uma URL qualquer em tempo de execução.

Exemplificando o ClassLoader em Java

Antes de explicarmos o funcionamento de cada ClassLoader nativo do Java, temos que entender o porquê de seu uso ser tão necessário.

O ClassLoader, por padrão, carrega todas as classes que estão presentes no seu CLASSPATH. Então não precisamos 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 que a Classe Funcionario da Aplicação 001 é diferente da Classe Funcionario da Aplicação 002.

Os ClassLoaders são úteis quando trabalhamos com várias aplicações com versões de bibliotecas diferentes. Imagine a aplicação 001 que trabalha 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, mas com ClassLoaders distintos podemos fazer isso.

No código a seguir temos o carregamento/loading de uma classe na memória:

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

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 à nossa classe carregada também devem ser carregadas, como se fosse um “cascade” de loadings.

Mecanismos de ClassLoader

Temos três tipos de ClassLoaders nativos: Bootstrap ClassLoader, Extension ClassLoader e Application ClassLoader. Cada um desses tem sua respectiva função:

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

Podemos dar um exemplo mais completo carregando uma classe dinamicamente através do loadClass, como mostra o exemplo da Listagem 1.

Listagem 1. Carregando Classe


 
 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 apresentado 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, como mostra a Listagem 2.

Listagem 2. 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;
     }
 
 }

Quando queremos carregar a classe, essa deve estar dentro do pacote reflection e utilizamos o nome MyObject numa URL própria para isso. Caso contrário, passaremos a responsabilidade para o ClassLoader parent.

Na Listagem 3 vamos criar um método main que utilizará nosso ClassLoader.

Listagem 3. 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();
     
 }
 
 }

Primeiramente capturamos o parent do nosso ClassLoader (MyClassLoader) do nosso ClassLoader. Então criamos uma instância dele, passando como parâmetro o seu parente, assim poderá delegar a função de loadClass para o parente, caso necessite.

Carregamos a classe “reflection.MyObject” no nosso novo ClassLoader (lembre-se que nessa hora ele carregará a classe de uma URL específica, que é diferente do nosso CLASSPATH). Agora temos a classe carregada em um classLoader próprio e podemos então recarregar a mesma, fazer um reloading e um “loadClass”.

Raramente precisaremos criar o nosso próprio ClassLoader ou nunca precisar trabalhar com ele diretamente, mas a sua utilidade como um todo é essencial e o conhecimento desse é um diferencial. Você poderá resolver problemas como “NoClassDefFoundError” em pouco tempo, apenas conhecendo o princípio básico do funcionamento do ClassLoader. Nesse caso, a classe pode existir fisicamente, mas em outro ClassLoader, que não é o mesmo que a aplicação está utilizando.

Esses conceitos, mesmo não sendo utilizados na prática, são importantes e podem abrir um leque de opções ao tentar entender problemas que ocorrem frequentemente.