Há algumas semanas publiquei um post que ensinava como ordenar os JARs das aplicações web (toda e qualquer aplicação dentro do diretório webapps do tomcat) através da manipulação do WebappClassloader, porém fiquei devendo falar sobre os outros classloaders presentes no tomcat, particularmente os que mais interessam são o common classloader e o shared classloader – não sabe o que são classloaders, ou não sabe o papel de cada um desses no tomcat? -> Understanding classloaders.
Achei que modificá-los seria uma tarefa mais fácil que enfrentei para manipular o loading das webapps. Definitivamente não! A começar que DocJar, muita reflexão e java.lang.reflect.* foram precisos para hit-the-goal.
JAVA Reflection!!?? Pra quê??? Vou explicar… A partir de gora leia (e talvez releia) com calma porque a priori pode parecer um pouco complicado por conta do vai-pra-lá-vem-pra-cá das classes das quais estarei falando a seguir.
Existe um tal de StandardClassLoader (org.apache.catalina.loader) que é a classe que irá implementar os três classloaders, do tomcat, pouco conhecidos mas essenciais para o seu funcionamento, a saber: common, shared e o server (também conhecido como o catalina classloader). E mais uma vez repito, se não tiver idéia do papel desses classloaders -> Understanding classloaders (é condição sine qua non para você entender este post). Enfim, voltando, essa classe extende o, largamente utilizado, URLClassLoader (java.net) o qual realiza suas tarefas lançando mão de um atributo do tipo URLClassPath (sun.misc) que realiza o trabalho pesado. Porém, se vocês tiverem a curiosidade, vejam que o atributo URLClassPath (o ucp) tem visibilidade default de forma que o mesmo não é acessível em uma subclasse de outro pacote. E infelizmente, como eu disse antes, o ucp realiza uma boa parte do trabalho atribuido ao URLClassLoader então como manipulá-lo? FOI TRISTE para nós!? Não!
Agora entra em cena as armas fornecidas pelo pacote java.lang.reflect. UM MOMENTO! Para o entendimento desse post é necessário que você saiba trabalhar bem com reflection, se não está seguro disso, consta aqui no publicclass{} um breve post que mostra como acessar métodos, atributos e inner classes inacessíveis (leia-se privados ou protected). Isso será suficiente para compreender os próximos parágrafos…
Mãos a obra! Objetivo: ordenar/adicionar, dinamicamente, os resources e .class da forma como desejarmos, dessa forma a versão da classe, ou um arquivo qualquer (resource), que desejamos esteja visível no escopo do classloader. Para isso precisaremos modificar o comportamento do URLClassPath. O principal método é o setRepositories, este realizará a reordenação da pilha mantida pelo URLClassPath, pois esta será a ordem a qual o URLClassLoader irá carregar os JARs (.class e resources se o programador achar necessário, no caso apresentado aqui só os jars serão carregados por esses classloaders). Abaixo consta a classe StandardClassLoader que, como dito antes, é responsável por implementar os três classloaders citados no início.
classe: org.apache.catalina.loader.StandardClassLoader jar: bin/bootstrap.jar private void setRepositories() { URL[] urls = getURLs(); Arrays.sort(urls, comparator); try { InputStream is = loadOrderedRepositoriesFile(); try { DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); DocumentBuilder db = dbf.newDocumentBuilder(); Document doc = db.parse(is); doc.getDocumentElement().normalize(); NodeList nodeList = doc.getElementsByTagName("repository"); ArrayList<URL> urlList = new ArrayList<URL>(nodeList.getLength() + urls.length); int lastJarEntryFromLibIndex = findLastJarEntryFromLib(urls); for (int i = 0; i < urls.length; i++) urlList.add(urls[i]); if (!classloaderLibPath.exists()) new FileNotFoundException(name + " directory does not exists"); for (int i = 0; i < nodeList.getLength(); i++) { String path = nodeList.item(i).getTextContent(); if (log.isDebugEnabled()) log.debug(" Including directory glob " + classloaderLibPath.getAbsolutePath() + File.separatorChar + path); File actualDirectory = new File(classloaderLibPath, path); if (!actualDirectory.exists() || !actualDirectory.isDirectory()) continue; File[] files = actualDirectory.listFiles(fileFilter); for (int j = 0; j < files.length; j++) { if (log.isDebugEnabled()) log.debug(" Including glob jar file " + files[j].getAbsolutePath()); // adicionar repositorio a partir de /lib e nao antes de incluir endorsed, i18n ou classes urlList.add(lastJarEntryFromLibIndex++, files[j].toURI().toURL()); } } urls = urlList.toArray(new URL[urlList.size()]); putURLStackOrderedByMyWay(urls); log.info("Urls added to " + name + " correctly"); } catch (Exception e1) { log.error(e1); } } catch (FileNotFoundException e2) { log.warn("No ordered-repositories.xml not found for " + name + " Classloader. Any extra repositories where configured."); try { putURLStackOrderedByMyWay(urls); log.info("Urls added to " + name + " correctly"); } catch (Exception e3) { log.error(e3); } } }
Se vocês repararem no StandardClassLoader original verão que o mesmo não implementa nenhuma funcionalidade, simplesmente extendeURLClassLoader e esta é a verdadeira responsável por carregar as classes solicitadas a ele. Houve a necessidade de implementar vários métodos mas nenhum foi sobrescrito, pois as mudanças foram de fora para dentro. Vou explicar melhor. Para atingir nosso objetivo, sobrescrever um método de URLClassLoader não era suficiente, pois era necessário modificar o comportamento da URLClassPath e isso só poderia ser feito via reflection ou recompilando a mesma e “inputando-o” num jar. A primeira opção pareceu ser mais interessante pelo fato de que só seria necessário alterar uma classe ao invés de outras e alterar somente um jar o bootstrap.jar.
Vamos ao que interessa. O método loadOrderedRepositoriesFile() basicamente carrega o XML ordered-repositories.xml que conterá as informações necessárias para carregar os diretórios (os jar e arquivos contidos no mesmo). O findLastJarEntryFromLib é importante, ele é responsável por retornar a posição (num array de URLs) que está o último jar contido no diretório lib do classloader (common/lib, server/lib ou shared/lib). Isso é importante pois todos os repositórios serão ordenados com prevalência sobre tudo que houver dentro de classloader-name/lib (classloader-name = common|shared|server). Ora, nada mais justo! A não ser que o programador queira sobrescrever uma das classes essenciais do tomcat (o que pode realmente acontecer, mas não é recomendado que esta seja carregada dentro do diretório lib e sim no seu diretório padrão), por exemplo: servlet-api.jar, o jsp-api.jar para que irá usar JSP (a maioria). Com a informação do índice do último jar do constante no lib podemos enfim adicionar nossos jars a diretórios dentro de lib.
urlList.add(lastJarEntryFromLibIndex++, files[j].toURI().toURL());
Isso é feito nessa linha acima. Após adicionar todos os jars configurádos para isso no ordered-repositories.xml o método putURLStackOrderedByMyWay é necessário para “inputar” a Stack<URL> no atributo urls específico do URLClassPath. O ponto principal da rotina é aí, tal array de URLs lógico estará ordenado como queiramos, logo, quando a aplicação solicitar uma classe, a busca será realizada nesse array o qual estará estruturado conforme a vontade do programador. Abaixo se pode ver o método responsável por isso.
private void putURLStackOrderedByMyWay(URL[] urls) throws SecurityException, NoSuchFieldException,
IllegalArgumentException, IllegalAccessException {
URLClassPath ucp = (URLClassPath) getUcpField().get(this);
Field urlsField = URLClassPath.class.getDeclaredField("urls");
urlsField.setAccessible(true);
Stack urlStack = (Stack) urlsField.get(ucp);
// limpo a pilha de url do URLClassPath para preencher com a ordem que eu preciso
urlStack.removeAllElements();
// como uma pilha é FIFO tenho que por os primeiros da lista por ultimo
for (int i = urls.length - 1; i >= 0; i--)
urlStack.add(urls[i]);
// simula um ucp.setURLs(stack)
urlsField.set(ucp, urlStack);
}
Enfim pessoal, por enquanto é isso. Creio que cumpri a missão de passar uma boa idéia de como realizar essa tarefa que ao meu ver é de muita importência. O código fonte pode ser baixado para os que quiserem analisá-lo para melhor compreensão, afinal um post detalhado custaria nó mínimo mais umas 200 linhas.
Source: ordenar-classloaders-src.rar
Tags: bootstrap, catalina, classloader, classloader hell, common, jar, jar loading, JAVA, order, repository, server, shared, tomcat, tomcat5