No post anterior tratei de como funcionam os classloaders, algo que tira o sono de muitos programadores JAVA. Se você, leitor, não entende bem o que são os classloaders ou como é o modelo de delegação num servidor de aplicação (que implementa a partir da especificação Servlet Specification versão 2.3, vide seção 9.7.2) recomendo que leia o post anterior (Understanding Classloaders) para que possa compreender os conceitos que serão expostos a seguir.
Para você não perder seu precioso tempo lendo o que possa não o interessar, vou fazer uma pergunta e se sua resposta for “sim” este post é sim do seu interesse: você já precisou forçar a ordem de loading de um JAR na sua aplicação web (leia-se dentro do diretório webapps) para que uma classe X específica (ou mais de uma) definida em um ou mais JARs fosse carregada? Se não entendeu, vou ser mais claro. Vamos supor que há a classe foo.X presente no arquivo app1.jar e presente também no app2.jar e ambos JARs estão no /WEB-INF/lib da sua aplicação mas você redefiniu sua classe foo.X no app2.jar e precisa que ela seja carregado (para que esta versão do tipo seja a carregada pela JVM) antes do app1.jar pois a foo.X deste último está depreciada. No comportamento default do tomcat o app1.jar será carregado antes do app2.jar o que tornaria sua empreitada um fracasso. Diante do cenário exposto posso agora perguntar novamente: você já se deparou com essa situação?
– SIM!!!
Então vamos lá…
Durante todo este post sempre que tratar de tomcat será o tomcat 5.5, no entanto o que for dito aqui funciona para o tomcat 6 e muito provavelmente para as futuras versões, mantendo-se as devidas restrições de implementação, claro.
Foi necessário fuçar o código que implementa o tomcat – algumas partes em específico, como o bootstrap.jar e principalmente o catalina.jar – para que pudesse entender como este realizava a rotina de loading dos arquivos JAR e class contidos nos vários repositório existentes.
Após muitas xícaras de café, googling, deep inside tomcat source consegui resolver um problema que tenho certeza que afeta programadores J2EE de aplicações de nível alto. Modificando o código do tomcat foi possível ordenar da maneira como quis o loading dos JARs – o que também vale para os class e os resources. Existe um método na class org.apache.WEBCLASSLOADER setRepositories() que é o ponto x da questão tratada neste post. É ele – depois de alterado – que será mostrado a seguir.
Relembrando, nós queremos carregar o app2.jar antes de carregarmos o app1.jar. E nesse ponto você já deve estar se perguntando: Então como JARs específicos serão carregados antes de outros? Bem, há infinitas maneiras do programador implementar isso, o que vocês verão a seguir é somente uma das maneiras (a qual eu considero bastante interassante, visto que é configurável via um arquivo XML). A modificação que fizemos na rotina setRepositories() consiste em adiar o loading dos resources contidos no diretório /WEB-INF/lib, pois por default ele é carregado logo depois do diretório /WEB-INF/classes. Veja o código:
classe: org.apache.catalina.loader.WebappLoader jar: server/lib/catalina.jarprivate void setRepositories() { ... String libPath = "/WEB-INF/lib"; classLoader.setJarPath(libPath); InputStream is = servletContext.getResourceAsStream("/WEB-INF/ordered-repositories.xml"); if(is != null){ try { DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); DocumentBuilder db = dbf.newDocumentBuilder(); Document doc = db.parse(is); doc.getDocumentElement().normalize(); NodeList nodeList = doc.getElementsByTagName("repository"); for (int i = 0; i < nodeList.getLength(); i++) { Node node = nodeList.item(i); String path = node.getTextContent(); loadRepository(servletContext, workDir, resources, libPath + "/" + path); } } catch (ParserConfigurationException e) { log.error(e); } catch (SAXException e) { log.error(e); } catch (IOException e) { log.error(e); } } // Setting up the JAR repository (/WEB-INF/lib), if it exists loadRepository(servletContext, workDir, resources, libPath); }
Veja também o conteúdo do arquivo ordered-repositories.xml contido no /WEB-INF da aplicação WEB:
<?xml version="1.0" encoding="ISO-8859-1"?>
<repositories>
<repository>app2</repository>
<repository>app1</repository>
</repositories>
A idéia é bastante simples, o arquivo ordered-repositories.xml é lido e dentro dele procura-se por tags <repository> que representam diretórios dentro de /WEB-INF/lib e todos as classes e resources que estiverem contidos nesse diretório serão carregados na ordem que aparecem no XML. A cada <repository> encontrada é enviada a ordem ao tomcat de carregar os arquivos contidos dentro do diretório contido no text da tag correspondente, ou seja, no exemplo acima o tomcat irá carregar /WEB-INF/classes, /WEB-INF/lib/app2, /WEB-INF/lib/app1 e /WEB-INF/lib nessa ordem.
Dessa forma basta configurar o seu build.xml para que o deploy jogue a app2.jar dentro de /WEB-INF/lib/app2 e o app1.jar dentro de /WEB-INF/lib/app1 ou até mesmo dentro de /WEB-INF/lib e remover a entrada <repository>app1</repository> do ordered-repositories.xml, pois o tomcat irá carregar somente o /WEB-INF/lib/app2 e passará para carregar o /WEB-INF/lib onde o app1.jar estará presente. Portanto, o app2.jar sendo carregado antes do app1.jar você terá, enfim, o que desejava: o controle de qual implementação de classes presentes nos dois (ou mais) JARs será o carregado pela JVM. Vale relembrar que esse comportamento da JVM foi explicado no post anterior (Understanding Classloaders), portanto se tiver dúvidas acerca de como é o modelo de delegação, vale a pena dar uma lida.
Até mais!
Tags: classloader, classloader hell, jar, jar loading, JAVA, order, repository, tomcat, tomcat5
August 13, 2009 at 01:59 |
ótimo post!!!!!!!
era exatamente oq eu estava precisando!!!!!!
vasculhei em toda internet em busca do assunto tratado nesse post e não encontrava nada.
obrigado pela contribuição
September 11, 2009 at 16:34 |
[...] algumas semanas publiquei um post que ensinava como ordenar os JARs das aplicações web (toda e qualquer aplicação dentro do [...]