sábado, 26 de janeiro de 2013

Isolamento de classloaders no JBoss AS 7


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