INTRODUÇÃO
Nesse
artigo vamos mostrar o isolamento de classloaders no JBoss AS 7. Esse
isolamento permite eliminar erros chamados de “Classloader Hell”
que pode ocorrer nas seguintes situações:
- Se em tempo de compilação usarmos uma biblioteca de terceiros, como a Apache Commons na versão 1.2.7 (por exemplo), mas em tempo de execução no servidor de aplicações a biblioteca usada estiver na versão 1.2.6 e classe em questão não possuir o método que existe na versão 1.2.7 pode levar a JVM do servidor a lançar a exceção java.lang.NoSuchMethodException.
- Ou então, se a biblioteca Apache Commons usada em tempo de execução for de versão diferente da versão usada em tempo de compilação, mas os métodos usados na aplicação existem nas duas versões da biblioteca, isso pode levar a comportamentos anêmicos, pois no desenvolvimento usamos uma implementação da versão da biblioteca, mas na execução, usamos outra implementação e mesmo que aparentemente não ocorra exceções como java.lang.NoSuchMethodException, java.lang.ClassNotFoundException, java.lang.NoClassDefFoundError e outras exceções semelhantes, o comportamento da aplicação será imprevisível.
- Como os classloaders seguem uma hierárquia na qual, um classloader delega a chamada para resolver a classe para outro classloader, temos o problema de um determinado classloader carregar uma classe de uma biblioteca e outra classe de outra biblioteca, que possuam os nomes completos qualificados iguais. Isto é, teremos aplicações que a principio, deveriam carregar todas as classes da mesma biblioteca, mas na realidade (em tempo de execução), há um carregamento parcial de classes de um jar e outro carregamento parcial de outro jar, e assim, gerando um comportamento imprevisível.
Então,
para eliminar esses problemas, a equipe de desenvolvimento do JBoss
AS, resolver reescrever o servidor aplicações JBoss AS para
eliminar esses erros recorrentes de classloaders na versão 7.x.
PREPARAÇÃO
DO AMBIENTE
Para
testar / analisar esse isolamento, vamos criar o projeto EAR de nome
“TestEAR” e cujos módulos serão mostrados na figura a seguir.
Nesse figura temos o projeto “TestEAR” contendo 2 projetos EJB's que são “Test1EJB” e “Test2EJB”. E dois projetos web que são “Test1WAR” e “Test2WAR”. A seguir será mostrado o código do arquivo “application.xml” que define os módulos dentro do EAR.
<xml
version="1.0"
encoding="UTF-8"?>
<application
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:application="http://java.sun.com/xml/ns/javaee/application_5.xsd"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/application_6.xsd"
id="Application_ID"
version="6">
<display-name>TestEAR</display-name>
<initialize-in-order>true</initialize-in-order>
<module>
<ejb>Test1EJB.jar</ejb>
</module>
<module>
<ejb>Test2EJB.jar</ejb>
</module>
<module>
<web>
<web-uri>Test1WAR.war</web-uri>
<context-root>Test1WAR</context-root>
</web>
</module>
<module>
<web>
<web-uri>Test2WAR.war</web-uri>
<context-root>Test2WAR</context-root>
</web>
</module>
</application>
E também,
como estamos usando o servidor de aplicações JBoss AS 7,
necessitamos usar o arquivo “jboss-deployment-structure.xml”
que define as dependências entre módulos, exclusão de pacotes, uso
de módulos do jboss, etc. Esse arquivo deve estar na pasta raiz do
projeto EAR ou na pasta META-INF do projeto EAR junto com o arquivo
“application.xml”. Esse arquivo será mostrado a seguir.
<xml
version="1.0"
encoding="UTF-8"?>
<jboss-deployment-structure
xmlns="urn:jboss:deployment-structure:1.1">
<ear-subdeployments-isolated>false</ear-subdeployments-isolated>
<deployment>
<dependencies>
<module
name="javax.api"
/>
<module
name="deployment.TestEAR.ear.Test1EJB.jar"/>
<module
name="deployment.TestEAR.ear.Test2EJB.jar"/>
<module
name="deployment.TestEAR.ear.Test1WAR.war"/>
<module
name="deployment.TestEAR.ear.Test2WAR.war"/>
</dependencies>
</deployment>
<sub-deployment
name="Test1WAR.war">
<dependencies>
<module
name="deployment.TestEAR.ear.Test1EJB.jar"/>
<module
name="deployment.TestEAR.ear.Test2EJB.jar"/>
</dependencies>
<local-last
value="true"/>
</sub-deployment>
<sub-deployment
name="Test2WAR.war">
<dependencies>
<module
name="deployment.TestEAR.ear.Test1EJB.jar"/>
<module
name="deployment.TestEAR.ear.Test2EJB.jar"/>
</dependencies>
<local-last
value="true"/>
</sub-deployment>
</jboss-deployment-structure>
Projeto “TestLib”
É um
projeto Java simples que conterá uma classe utilitária para obter
informações do classloader de outros projetos e também, para obter
informações do seu próprio classloader. Na figura a seguir, será
mostrado a estrutura do projeto “TestLib”.
Note que
no projeto “TestLib”, só temos a classe TesteUtil
cujo código será mostrado a seguir:
package utils;
import
java.util.Collections;
import
java.util.LinkedList;
import java.util.List;
public class TesteUtil
{
static {
StringBuilder str =
new StringBuilder();
str.append("\nTesteUtil
- Carregamento da classe - INICIO\n");
str.append(getInfoClassLoader());
str.append("\nTesteUtil
- Carregamento da classe - FIM");
System.out.println(str.toString());
}
public static void
main(String... strings) {
System.out.println("Classloader:
");
System.out.println(getInfoClassLoader());
}
public static String
getInfoClassLoader(ClassLoader cl) {
Class<?> cls =
cl.getClass();
StringBuilder str =
new StringBuilder();
// Nome da classe e
hashcode do objeto ClassLoader
str.append("\nClasse:
" + cls.getName() + " - " + cl.hashCode() + "\n");
return
str.toString();
}
public static String
getInfoClassLoader() {
ClassLoader cl =
TesteUtil.class.getClassLoader();
return
getInfoClassLoader(cl);
}
}
Note que
nessa classe “TesteUtil” temos um bloco anônimo estático
justamente para que possamos obter informações do classloader do
projeto “TestUtil” que será o mesmo do projeto EAR, pois
esse projeto Java será mais um “jar” dentro da pasta “EAR/lib”
do projeto EAR.
Projetos
“Test1EJB” e “Test2EJB”
A
estrutura do projeto “Test1EJB” será mostrado na figura a
seguir, lembrando que o projeto “Test2EJB” é análogo
(semelhante) ao projeto “Test1EJB” com a diferença nos
nomes, ao invés de usar “Test1”, usa-se “Test2”,
de outra maneira, só trocarmos o número “1” pelo número
“2”.
Note que
o projeto “Test1EJB” possui uma interface de nome “Teste1”
que define um método para obter informações do classloader do
projeto EJB. A seguir será mostrado o código dessa interface
(arquivo “Teste1.java”).
package
ejb;
import
javax.ejb.Remote;
@Remote
public
interface
Teste1 {
public
String getInfoClassLoader();
}
E para o EJB
“Teste1Bean” que implementa a interface “Teste1” cujo código
será mostrado a seguir:
package ejb;
import
javax.annotation.PostConstruct;
import
javax.ejb.Startup;
import
javax.ejb.Stateless;
import utils.TesteUtil;
@Startup
@Stateless
public class Teste1Bean
implements Teste1 {
@Override
public String
getInfoClassLoader() {
return
TesteUtil.getInfoClassLoader(this.getClass().getClassLoader());
}
@PostConstruct
public void
printClassloader() {
StringBuilder str =
new StringBuilder();
str.append("\nClassloader
- PostConstruct1 - INICIO\n");
str.append(getInfoClassLoader());
str.append("Classloader
- PostConstruct1 - FIM\n");
System.out.println(str.toString());
}
}
Projetos
“Test1WAR” e “Test2WAR”
Esses são
dois projetos web que usaram os projetos EJB's e o projeto Java
“TestLib”. A estrutura do projeto “Test1WAR”
será mostrado a seguir. Lembrando que o projeto “Test2WAR”
é análogo (semelhante) ao projeto “Test1WAR”, portanto,
toda configuração do “Test1WAR” vale para o projeto
“Test2WAR” com a diferença nos nomes, basta trocar o
número “1” por “2”.
Note que
nesse projeto web, precisamos criar um ServiceLocator para
facilitar a localização de EJB's dos outros projetos e também,
criamos um Servlet para poder ser chamado via url no browser e
cujo código fará chamadas nos projetos EJB's, Java e no próprio
projeto web para obter informações a respeitos do classloaders dos
projetos. A seguir será mostrado o código do ServiceLocator e
depois, do Servlet “Test1WARServlet”.
package
test.web.base;
import
javax.naming.InitialContext;
public
class
ServiceLocator {
private
InitialContext jndiContext;
private
static
ServiceLocator instance;
private
static
final
String PREFIX_GLOBAL
= "java:global/";
//
private static final String PREFIX_APP = "java:app/";
private
String earJndiName
= "";
private
ServiceLocator() {
try
{
jndiContext
= new
InitialContext();
}
catch
(Exception e) {
e.printStackTrace();
}
}
public
static
ServiceLocator getInstance() {
if
(instance
== null)
{
instance
= new
ServiceLocator();
}
return
instance;
}
public
String getEarJndiName() {
return
earJndiName;
}
public
void
setEarJndiName(String earJndiName) {
this.earJndiName
= earJndiName;
}
public
Object get(final
String jndiName) {
Object
localHome = null;
try
{
localHome
= jndiContext.lookup(PREFIX_GLOBAL
+ earJndiName
+
jndiName);
}
catch
(Exception e) {
e.printStackTrace();
}
return
localHome;
}
}
package
test.web.servlet;
import
java.io.IOException;
import
javax.servlet.ServletException;
import
javax.servlet.http.HttpServlet;
import
javax.servlet.http.HttpServletRequest;
import
javax.servlet.http.HttpServletResponse;
import
test.web.base.ServiceLocator;
import
utils.TesteUtil;
import
ejb.Teste1;
import
ejb.Teste2;
/**
* Servlet implementation class
Test1WARServlet
*/
public
class Test1WARServlet extends HttpServlet {
private
static final long serialVersionUID = 1L;
/**
* @see HttpServlet#HttpServlet()
*/
public
Test1WARServlet() {
super();
}
/**
* @see HttpServlet#doGet(HttpServletRequest request,
HttpServletResponse
* response)
*/
protected
void doGet(HttpServletRequest request,
HttpServletResponse
response) throws ServletException, IOException {
StringBuilder
str = new StringBuilder();
str.append("\nTest1WARServlet
- INICIO\n");
str.append("\nClassloader
- Lib\n");
str.append(TesteUtil.getInfoClassLoader());
str.append("\nClassloader
- WAR\n");
str.append(TesteUtil.getInfoClassLoader(this.getClass()
.getClassLoader()));
ServiceLocator
serviceLocator = ServiceLocator.getInstance();
serviceLocator.setEarJndiName("TestEAR/Test1EJB/");
Teste1
serviceTeste1 = (Teste1) serviceLocator.get("Teste1Bean");
str.append("\nClassloader
- Teste1EJB\n");
str.append(serviceTeste1.getInfoClassLoader());
serviceLocator.setEarJndiName("TestEAR/Test2EJB/");
Teste2
serviceTeste2 = (Teste2) serviceLocator.get("Teste2Bean");
str.append("\nClassloader
- Teste2EJB\n");
str.append(serviceTeste2.getInfoClassLoader());
str.append("\nTest1WARServlet
- FIM");
System.out.println(str.toString());
}
/**
* @see HttpServlet#doPost(HttpServletRequest request,
HttpServletResponse
* response)
*/
protected
void doPost(HttpServletRequest request,
HttpServletResponse
response) throws ServletException, IOException {
}
}
A seguir,
mostraremos o código do arquivo “web.xml”do projeto web.
Nesse arquivo definimos o Servlet criado para que possa estar
disponível para receber requisições HTTP.
<xml
version="1.0"
encoding="UTF-8"?>
<web-app
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
id="WebApp_ID"
version="2.5">
<display-name>Test1WAR</display-name>
<servlet>
<description></description>
<display-name>Test1WARServlet</display-name>
<servlet-name>Test1WARServlet</servlet-name>
<servlet-class>test.web.servlet.Test1WARServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>Test1WARServlet</servlet-name>
<url-pattern>/teste1</url-pattern>
</servlet-mapping>
</web-app>
Com isso, para acessar
o servlet, só clicar nesses links:
Lembrando
que o IP do servidor é 127.0.0.1 e porta 8080. Caso esteja rodando
servidor JBoss AS 7 em outra máquina, usar o IP dessa máquina para
acessar o Servlet.
RESULTADOS
Ao
executarmos o mesmo projeto EAR com ou sem isolação (tag
ear-subdeployments-isolated do arquivo
jboss-deployment-structure.xml), o JBoss AS 7 coloca uma instância
de classloader (classe org.jboss.modules.ModuleClassLoader) para cada
unidade de deploy dentro do EAR. Isto significa que o projeto EJB
terá suas classes carregadas por uma instância de classloader, o
projeto WAR terá suas classes carregadas por outra instância de
classloader, pasta EAR/lib por outra instância e assim por diante
para outros projetos EJB / WAR existentes dentro do EAR. Então, para
melhor ilustração desse fato, veja na figura a seguir:
Consequentemente,
essa tag (ear-subdeployments-isolated) apenas isola o uso das
classes carregadas por cada unidade de deploy. Se essa tag estiver
configurada como “true” teremos que definir as
dependências entre unidades de deploy explicitamente no arquivo
“jboss-deployment-structure.xml”, caso contrário, o
servidor JBoss AS 7 lançará a exceção
java.lang.ClassNotFoundException. Veja na figura a seguir como
que as unidades de deploy se relacionam quando essa tag está com
valor “true”.
Note que
os projetos web “WAR1” e “WAR2” depende (pode acessar) os
projetos EJB1, EJB2 e pasta EAR/Lib. Somente esse acesso é possível,
se o EJB1 precisar acessar o EJB2, essa configuração precisa ser
declarada explicitamente no arquivo “jboss-deployment-structure.xml”.
Se
configurar essa tag (ear-subdeployments-isolated) como
“false”, todas as unidades de deploy poderão usar as
classes carregadas em outras unidades de deploy, sem restrição. Mas
nessa configuração teremos que tomar cuidado para uma unidade de
deploy não chamar classes de outras unidades de deploy que serão
implantadas depois da unidade de deploy atual. Por exemplo se um
projeto WAR chama uma classe do projeto EJB sendo que esse último
projeto (unidade de deploy) não foi implantado ou será implantado
depois do projeto WAR, então poderá ocorrer exceções como
java.lang.ClassNotFoundException. Veja na figura a seguir como
as unidades de deploy se relacionam quando essa tag possui o valor
“false”.
Note que
nessa configuração, todas as unidades de deploy (EJB1, EJB2, WAR1,
WAR2 e EAR/Lib) podem acessar qualquer unidade de deploy no EAR.
Para
evitar esse tipo de problema para o atributo configurado como
“false”, deve-se configurar a tag “initialize-in-order”
no arquivo application.xml com o valor “true”.
Lembre-se que essa tag só está definida na versão 6 do JavaEE. Com
essa configuração, forçaremos a implantação das unidades de
deploy definidas no arquivo application.xml na ordem descrita
nesse arquivo. Se não configurar essa tag com o valor “true”,
o servidor JBoss AS 7 carregará as unidades de deploy na ordem que
achar conveniente (digo, políticas implementadas dentro do JBoss AS
7, como por exemplo, tamanho dos módulos, tipo, prioridade de
implantação entre módulos, etc) e não a ordem no documento xml.
REFERÊNCIAS
Ótima explicação!!
ResponderExcluirParabéns pelo post!!