Há algum tempo (mais especificamente quando comecei a achar que já sabia o básico de Java e que precisava tentar algo realmente difícil, mesmo que não tão útil), lá pelos idos de 2006 ou 2007, resolvi que iria desenvolver um cliente de email, com algumas das funcionalidades úteis do que eu conhecia na época (Outlook, alguém conhece?)… Comecei a estudar os protocolos SMTP e POP3 (não conhecia o javax.mail) e comecei a brincar com Sockets (isso, se não tem algo mais simples vai na marra mesmo. Damn it!! I’ll do it by myself!!), como tive que fazer com o HTTP pra um trabalho de faculdade certa vez… Depois de algum tempo brincando com isso foi que conheci a javax.mail (para mais informações: javax.mail) e resolvi poupar massa cinzenta…
Mas enfim, o que me motivou a escrever este post não foi exatamente isso, mas sim o fato de que eu queria que o software tivesse algum diferencial, algo que o tornasse útil pelo menos pra mim (se outras pessoas também gostassem seria lucro), daí pensei em fazer o software e depois criar plugins para ele (só não fazia a menor idéia de como faria isso, mas faria)… Cheguei a implementar as classes base, mas acabei deixando de lado pois não sabia como implementar o sistema de plugins (além da falta de tempo e do excesso de preguiça que também colaboraram rsrs)…
Essa semana estava pensando em como seria pra montar esse esquema para adicionar plugins sem precisar modificar meu código inicial… Depois de pensar, cheguei na seguinte mecânica (a nível de usuário ainda, não de desenvolvedor): tenho uma aplicação em uma pasta, incluo uma pasta chamada plugins e incluo o jar do plugin ali.
Beleza, já sei como fazer isso “por fora”… Mas como eu faço isso dentro do sistema pra incluir isso em tempo de execussão? Pensei inclusive em criar um launcher (uma aplicação/script que é acionada e que fica responsável por chamar a minha aplicação) que buscaria todos esses arquivos jar presentes na pasta de plugins e incluindo na opção “-cp” do comando “java” (para saber mais: java command line) que chamaria a aplicação de verdade… Seria uma solução simples, mas algo nela não me agradou…
Depois de um tempo pensando no assunto me lembrei de um exemplo que vi de um CustomClassloader, onde poderíamos manipular algo no carregamento ou no comportamento das classes no sistema (acho que foi um exemplo de Programação Orientada a Aspecto). Comecei a pesquisa à partir daí…
Pesquisando um pouquinho (no nosso grande amigo Google), consegui encontrar uma solução para carregar os arquivos dinamicamente sem muitos problemas.
Simplificando um pouco, foi algo meio assim:
public class JarClassLoader extends URLClassLoader {
public JarClassLoader(URL[] urls) {
super(urls);
}
public void addFile(URL path) throws MalformedURLException {
super.addURL(path);
}
}
Antes de mais nada, o método addURL do URLClassLoader é protected, de modo que não poderia acessar em um URLClassLoader normal.
Daí pra buscar todos os arquivos foi simplesmente utilizar um objeto File mais ou menos assim:
URL[] urls = {};
JarClassLoader cl = new JarClasLoader(urls);
File pluginFolder = new File("./plugin");
File[] jars = pluginFolder.lisFiles();
for(File f: jars){
cl.addFile(f.toURI().toURL());
}
E assim carregamos todos os arquivos na pasta plugins, a não ser que hajam outros arquivos na pasta além dos seus jars, o que te obrigaria a implementar um FileFilter ou um FileNameFilter e passá-lo como parâmetro no método listFiles do objeto pluginFolder.
Depois disso eu já posso buscar minha classe com um loadClass do meu ClassLoader, passando como parâmetro o nome completo da classe que deseja carregar (devemos lembrar que não tem como carregar duas classes com mesmo full qualified name – ou, de forma mais simples, mesmo nome e mesmo pacote – ou carregar novamente uma com mesmos nome e pacote de algo que já foi carregado por um ClassLoader pai).
Class clazz = cl.loadClass(fullQualifiedClassName);
E com isso, agora já posso ter uma instância dessa classe.
Object obj = clazz.newInstance();
Beleza, já tenho como criar um objeto com o nome completo da classe… Mas e se eu não souber o nome dela de antemão? Ou mesmo que soubesse, como um Object me vai ser útil se eu não sei como utilizá-lo?
Pensei em algumas soluções. E em todas (ou quase todas, afinal largar isso tudo de lado e ir tomar um sorvete chegou a passar pela minha cabeça) elas havia a presença de uma interface. Por isso criei uma interface (InterfaceTeste).
public interface InterfaceTeste {
public abstract String getString();
}
Agora só falta a parte do nome da classe… Pensei em incluir um XML no jar, mas aí lembrei da minha ânsia de vômito quando vejo que tenho que criar um XML de configuração de alguma ferramenta (tudo isso graças ao finado Struts 1)… Facilitaria o trabalho e aumentaria o desempenho pelo fato de não precisar fazer uma busca no arquivo. Pensei em dar essa opção, o que não precisaria de mais nada, apenas utilizar o método getResourceAsStream do ClassLoader passando o nome do arquivo como poarâmetro e parsear o XML…
Parti para a parte (eu sei, ficou estranho, mas enfim, prossigamos) da busca das classes contidas no jar…
Mais um tempo no Google me rendeu bons frutos (como sempre)… Vi uma citação ao pacote java.util.jar… Olhada no Javadoc (acho que sejs válido uma olhadinha aqui: javadoc java.util.jar) encontrei algumas classes que tinham potencial, mas no fim utilizei apenas o JarFile e o JarEntry (que derivam de ZipFile e ZipEntry, respectivamente). Com isso consegui listar todos os arquivos contidos no jar (que não deixa de ser um zip).
JarFile jarFile = new JarFile(jarFile);
Enumeration jarEntrys = jarFile.entries();
Cada um desses JarEntry representa um arquivo dentro do seu jar. Utilizando o método getName deste objeto para obter o nome do arquivo (que consiste no caminho e o nome do arquivo – algo como ‘br/net/eldiosantos/MinhaClasse.class’). De posse dele, e sabendo que os pacotes, em Java, representam pastas, podemos pegar esse nome, verificar se termina por ‘.class’ e, se verdadeiro, retirar o ‘.class’ e substituir a ‘/’ por ‘.’. Assim já temos o nome das classes presentes no pacote.
E, por último, como eu iria trabalhar com uma interface (afinal nem todas as classes precisam implementar minha interface), eu verifico as classes uma a uma pra ver se alguma delas implementa a interface que quero.
Class clazz = cl.loadClass(nameClass);
Class[] interfaces = clazz.getInterfaces();
E assim resolvemos o problema…
Em breve disponibilizo minha implementação aqui (assim que achar que esta bom o suficiente).