|
|
Tiago Silveira
Uma introdução ao ClassLoader. Aprenda para que serve, como funciona e crie o seu próprio ClassLoader.
A chance de você um dia precisar escrever um classloader é pequena. Após ler este texto, você saberá quando é necessário ou mesmo útil criar seu próprio classloader.
Carga de código em Java
Arquivos fonte (.java) contêm texto que é transformado, através da compilação, em um código multiplataforma (bytecodes) e guardado em outro arquivo (.class). Para ter acesso a esse código, a JVM utiliza uma classe especializada chamada ClassLoader (do pacote java.lang). Como todas as classes Java são carregadas a partir de class loaders, a JVM utiliza um mecanismo de bootstraping parecido com o dos sistemas operacionais: carrega um primeiro class loader, conhecido como Root Class Loader, e utiliza esse class loader para carregar as outras classes no seu CLASSPATH.
Class Loaders têm a tarefa de verificar a segurança do código sendo executado, promovendo acesso aos certificados de autenticidade de código que permitem verificar sua procedência. Além disso, fazem parte do mecanismo de isolamento conhecido como Sandbox, pelo qual um módulo não deve ter acesso a recursos (classes, arquivos de configuração, ícones, etc) de outros módulos. Normalmente, uma aplicação pertence a um único módulo, porém contêiners como o Tomcat podem ter diversos módulos diferentes, cada um rodando uma "aplicação". Em browsers, cada applet tem seu próprio Sandbox, embora eles compartilhem a mesma JVM.
Mas a tarefa principal dos class loaders é funcionar como mapeadores de código: o código a que sua aplicação tem acesso é o código que está carregado pelo seu class loader. Veremos adiante como substituir um código que está rodando por outro mais novo em tempo de execução, ou por código que não está no seu CLASSPATH ou até mesmo por código que nem está na sua máquina.
Carregando classes a partir de um arquivo JAR
As classes básicas do Java, aquelas que já vêm com a distribuição, estão guardadas num arquivo jar. Portanto, o núcleo do Java precisa saber carregar recursos de dentro de um arquivo JAR (rt.jar).
Uma aplicação java deveria sempre estar dentro de um JAR. Ou de vários! Suponha que todas as classes do pacote br.guj.common.* estão no arquivo guj-common.jar.
Se vc coloca o guj-common.jar no seu classpath, vc não tem que fazer nada, o Root ClassLoader (também conhecido como Bootstrap Classloader) acha a classe pra vc sozinho.
Mas como proceder se o JAR não está no classpath, mas sim no seu diretório ${user.dir}/lib ("user.dir" é uma system property com o diretório de onde o programa foi iniciado)? Observe o pseudo-código abaixo:
1 java.net.URL location = new URL("jar:file://" + System.getProperty("user.dir") + "/lib/guj-common.jar!/");
|
Isso representa um diretório! Portanto, qualquer recurso pode ser acessado diretamente a partir dele, desde que vc tenha um ClassLoader disponível para tal.
O Java já vem com a classe java.net.URLClassLoader, vc pode criar um ClassLoader para a sua aplicação assim:
1 URL[] allLocations = new URL[1];
2 allLocations[1] = location;
3 ClassLoader loader = URLClassLoader.newInstance(allLocations, this.getClass().getClassLoader());
4
5 Class userList = loader.loadClass("br.guj.common.UserList", true);
6 List l = (List) userList.newInstance();
|
Suponha que br.guj.common.UserList é uma classes que implementa java.util.List.
Já usei um primeiro truque aqui: estabeleci um parentesco entre o Root ClassLoader (aquele que a JVM cria quando inicia) e o seu "loader": se uma classe que vc carregou do JAR precisar de uma classe que não está no JAR, mas está disponível no seu classpath, ela será encontrada. Esse é o caso da interface java.util.List.
Repare na importância de usar interfaces quando se deseja um código fácil de ler. Se você não pode utilizar uma interface, você vai ter que utilizar introspecção para acessar os métodos da sua classe "de fora".
RMI, Codebase e classloaders
Aplicações feitas com RMI utilizam um URLClassLoader como o nosso. Uma System Property "java.rmi.codebase" pode ser utilizada para dizer quais URLs devem ser utilizadas como base para a busca de recursos para um objeto remoto.
Internamente, a coisa funciona exatamente como vimos, com a diferença de que você não precisa manipular o class loader diretamente.
Escrevendo seu próprio class loader
Algumas raras vezes, você não quer utilizar nem o classpath nem arquivos JAR disponíveis num servidor web.
Com muito trabalho (um trabalho divertido, é verdade), você pode escrever um class loader que, antes de carregar uma classe, verifica se ela é a cópia mais nova do CVS, compila o fonte se necessário, e define a classe fresquinha!!
Na biblioteca SharedObjects (http://shob.sourceforge.net), um middleware para desenvolvimento de aplicações distribuídas, a localização do arquivo .class é desconhecida até o momento em que é necessária. O class loader descobre em que máquina está a classe, faz o download do arquivo e define a classe na hora em que ela é necessária pela primeira vez.
A forma ideal de criar um classloader é extendendo a classe java.lang.ClassLoader:
1 public class GujClassLoader extends java.lang.ClassLoader
|
É altamente recomendável ler a documentação da classe ClassLoader antes de criar o seu próprio. Um ClassLoader carrega classes pelo método loadClass(), que faz o seguinte:
chama o método findLoadedClass() para ver se a classe já está carregada.
chama o método loadClass() no classloader pai. No exemplo anterior, seria aqui que a interface java.util.List seria encontrada na hora de criar a classe UserList (que implementa List).
chama o método findClass() para encontrar a classe. É esse método que você vai sobreescrever.
Nosso exemplo utiliza um outro objeto para produzir os bytes que descrevem a classe. Poderia ser um código que faz o download do CVS ou de uma máquina remota, ou de um banco de dados, não importa, desde que os bytes venham:
1 public Class findClass(String name) throws ClassNotFound {
2 // lança ClassNotFoundException se o download falhar:
3 byte[] bytes = delegate.downloadClass(name);
4 Class c = super.defineClass(name, bytes, 0, bytes.length);
5 // Você pode fazer log, definir propriedades específicas de pacotes, etc;
6 return c;
7 }
|
Uma coisa legal é que você não precisa se preocupar em guardar a classe que você "encontrou", pois a implementação do ClassLoader mantém uma tabela para você.
Aqui não estamos lidando com um ProtectionDomain (java.security.ProtectionDomain), e sim usando as permissões "default". Isso significa que seu classloader não está preparado para lidar com pacotes assinados, por exemplo. Esse é um recurso avançado que exige uma abordagem própria.
Ciclo de vida de classes
Uma vez que você encontrou a classe, ela sobrevive como uma instância normal de objeto, isto é: enquanto houver referências para ela, ela não será coletada. Quem tem referências para o Class?
instâncias da classe
o class loader que a carregou.
Isso significa que, ao liberar o classLoader, as únicas referências para a classe serão as de suas instâncias. Se você souber quais são todas as instâncias daquela classe, pronto! Você pode reciclar tudo.
É o princípio utilizados por Servlet Containers como o Tomcat. Ao recarregar uma aplicação, ele pode jogar fora o contexto em que ela estava, com class loader junto, e inicializar tudo de novo.
Lembre-se que um ClassLoader pode ser referenciado indiretamente pelos seus filhos (i.e. outros class loaders que o tem como pai), o que pode impedir sua coleta.
Encontrando recursos com um classloader
Além de classes, seu class loader pode acessar outro recursos, como ResourceBundles, fontes, arquivos de configuração, etc.
Um class loader tem os métodos getResource() e getResourceAsStream() que podem encontrar recursos dentro do seu classpath. Para ter acesso ao conteúdo do arquivo que contém a classe String, podemos utilizar:
1 InputStream is = loader.getResourceAsStream("java/lang/String.class");
|
Se você tem um URLClassLoader associado a vários arquivos JAR, ele pode encontrar arquivos dentro dos jars sem você precisar fornecer o nome do JAR para cada um (aqui consideramos que as classes do br.guj.* estão em "WEB-INF/classes"):
01 String base = "jar:file://";
02 base += System.getProperty("catalina.home");
03 base += "/webapps/application.war!/";
04
05 List list = new ArrayList();
06 URL application = new URL(base);
07 URL classes = new URL(base + "WEB-INF/classes");
08 list.add(classes);
09 list.add(application); // deixaremos por último esse.
10
11 URL[] urls = (URL[]) list.toArray(new URL[list.size()]);
12 ClassLoader loader = new URLClassLoader(urls);
13 URL url = loader.getResource("br/guj/common/UserList.class");
14 System.out.println("UserList: " + url);
15
16 url = loader.getResource("web.xml");
17 System.out.println("WebXML: " + url);
|
A saída seria mais ou menos assim:
UserList: jar:file:///usr/local/tomcat/webapps/application.war!/WEB-INF/classes/br/guj/common/UserList.class
WebXML: jar:file:///usr/local/tomcat/webapps/application.war!/web.xml
|
Bem, acho que isso é tudo. Espero que este texto seja útil. Críticas, dúvidas e sugestões são bem-vindas!
|
|
|