Blog sobre desenvolvimento de software (Java, muito Java!), inovação tecnológica e cotidiano do Universo de TI. Acesse notícias, tutoriais, material de cursos e eventos, código, desafios, soluções, opiniões, pensamentos, divagações, balbuciações e abobrinhas diversas. Deixe seu comentário!

sábado, 29 de dezembro de 2012

JAR Hell - Um guia prático

O que é JAR Hell?


JAR Hell é um termo cunhado para descrever de forma genéricas problemas relacionados ao ClassLoader. Suas variações mais comuns acontecem quando:
    1. Duas ou mais classes diferentes, com o exato mesmo nome qualificado acabam sendo incluídas no projeto (e.g, duas versões diferentes da biblioteca ASM no Classpath).
    2. Duas ou mais bibliotecas no Classpath dependem de versões diferentes das mesmas classes (e.g., Hibernate e Spring dependendo de versões diferentes da biblioteca ASM).

      Sintomas do JAR Hell


      JAR Hells possuem algumas peculiaridades marcantes, mesmo assim nem sempre seu diagnóstico é óbvio; várias vezes uma situação de JAR Hell vem mixada com outros tipos de problemas como ausência de dependências transitivas no Classpath e omissões em arquivos de configuração.
      Eis alguns sintomas quem indicam um problema de JAR Hell:
      • Estão sendo disparados NoSuchMethodError, NoSuchFieldError ou IllegalAccessError.
      • ClassCastException entre classes iguais ou compatíveis.
      • Problemas de serialização / deserialização.
      Outra pista que aponta para uma situação de JAR Hell acontece quando um ou mais dos problemas mencionados acontecem de forma intermitente, e particularmente quando dão a impressão de estarem relacionados a questões de build / ambiente (e.g., aparecem no ambiente de homologação mas não no de desenvolvimento; builds pelo IDE funcionam / builds pela ferramenta de Integração Contínua não; com o servidor em modo de desenvolvimento tudo funciona mas em modo de produção não; com JVMs da Sun tudo funciona e com JRockit não).
      Isso ocorre pois quando há duas ou mais classes com o mesmo nome qualificado no Classpath, a lógica que determina qual delas será carregada fica codificada no ClassLoader utilizado, ou em última instância, na lógica dos ClassLoaders de base da própria JVM. 
      Prever qual versão de uma classe será carregada é uma tarefa difícil devido a cadeia complexa de ClassLoaders tipicamente presente em soluções enterprise. O próprio ClassLoader escolhido para abrir determinada classe pode variar conforme o estado do container e código da aplicação. 
      Em geral o ClassLoader que carrega uma classe também fica responsável por carregar todas as outras classes utilizadas por ela; essas classes e o valor de suas variáveis estáticas são então cacheados em memória (e.g., na famigerada PermGen). Isso significa que - salvo exceções em que o ClassLoader se torna elegível para Garbage Collection e a JVM é otimizada para fazer unloading de classes - uma vez que determinada versão de uma classe seja carregada ela permanece em memória, em um namespace relacionado ao ClassLoader, até fim do processo.

      Resolvendo Problemas de JAR Hell


      Os passos lógicos para resolver JAR Hells são:
      • No caso de classes duplicadas dentro do domínio da sua aplicação: Detectar e excluir versões antigas da classe duplicada. Preferencialmente deixar somente uma classe de cada tipo no Classpath, i.e., não há necessidade da mesma classe estar presente em vários JARS da aplicação, sendo recomendável refatorar as classes repetidas para um JAR a parte.
      • No caso de incompatibilidade entre bibliotecas de terceiros: Fazer upgrades e / ou downgrades das bibliotecas para uma combinação com dependências transitivas compatíveis. Uma ferramenta para gerenciar dependências ajuda muito.
      Se por algum motivo não for possível eliminar versões duplicadas de uma classe ou encontrar bibliotecas compatíveis, como último recurso é possível escrever ClassLoaders personalizados. Esse é um tópico avançado que fico devendo para um próximo post. Classifico o uso de ClassLoaders customizados como um último recurso pois eles aumentam a Entropia do Software, são de difícil manutenção e, se não forem bem arquitetados, podem gerar todo tipo de problemas (NoClassDefFoundError, java.lang.OutOfMemoryError: PermGen space, etc).

      Encontrando classes duplicadas


      Geralmente essas classes estão em bibliotecas do EAR (APP-INF/lib), WAR (/WEB-INF/lib), ou do Servidor de Aplicação.
      Para saber de que JAR / diretório determinada classe está sendo lida utilize os seguintes métodos:
      public static URL getLocation(Class clazz) {
          return clazz.getProtectionDomain().getCodeSource().getLocation();
      }
      public static URL getLocation(String className) 
          throws ClassNotFoundException {
          return getLocation(Class.forName(className));
      }
      Exemplos de chamada e resposta (para um projeto mavenized):
      System.out.println(getLocation(EntityManager.class)); 
      /* 
       * file:/home/anthony/.m2/repository/javax/persistence/
       * javax.persistence/2.0-SNAPSHOT/javax.persistence-2.0-SNAPSHOT.jar 
       */
      System.out.println(getLocation(
          "org.apache.commons.lang.builder.ReflectionToStringBuilder")); 
      /* 
       * file:/home/anthony/.m2/repository/commons-lang/commons-lang/
       * 2.6/commons-lang-2.6.jar 
       */
      Um bom lugar a ser analisado são clientes de Web services gerados automaticamente, principalmente quando mais de um serviço depende do mesmo XSD. Nesse cenário é comum que vários clientes sejam gerados a partir de WSDLs diferentes, todos contendo suas próprias versões da mesma classe. 
      Na verdade todo tipo de código gerado automaticamente merece atenção: Classes JAXB geradas com xjc, entidades JPA repetidas em diferentes projetos EJB, etc. 

      Problemas de compatibilidade entre bibliotecas de terceiros


      Para resolver esse tipo de JAR Hell é necessário encontrar versões compatíveis das bibliotecas (e.g., uma versão do Spring e do Hibernate que possam funcionar com a mesma versão da biblioteca ASM), ou, em casos de total incompatibilidade, escrever ClassLoaders customizados.
      Geralmente esse tipo de erro acontece com bibliotecas populares como como Apache Commons, Javassist e ASM que são dependências para uma grande quantidade de frameworks e bibliotecas.
      Devido a dependências transitivas (e.g., seu projeto depende da biblioteca A que por sua vez depende da biblioteca B que depende da biblioteca C e assim sucessivamente), encontrar versões compatíveis de bibliotecas pode se tornar uma tarefa enfadonha.  
      Para aliviar o trabalho do programador ferramentas poderosas como Maven, Ivy e Buildr estão disponíveis. Com essas ferramentas é possível especificar as bibliotecas (e versões / intervalos de versões) que seu projeto necessita em arquivos de configurações; essas ferramentas fazem então o download dos artefatos - e suas dependências transitivas - a partir de uma hierarquia de repositórios de software (como o gigantesco Repositório Central), ajustam o Classpath da aplicação para compilação / execução / testes e ainda empacotam bibliotecas dentro dos EARs, WARs, etc para distribuição quando cabível. 
      Caso ocorra algum conflito o desenvolvedor pode trocar facilmente a versão de determinada biblioteca no arquivo de configuração e deixar que a ferramenta faça o trabalho duro por ele.
      Enquanto uma ferramenta como o Maven não se aplica a todo tipo de projeto (pois introduz suas próprias complexidades), eu costumo usá-lo pelo menos para fazer download das dependências do projeto e exportá-las com o Dependency plugin. Fico devendo um tutorial sobre esse assunto para um próximo post.

      Bibliotecas do Servidor de Aplicação vs Bibliotecas do Projeto


      Pessoalmente tento, sempre que possível, usar bibliotecas do próprio servidor (que passaram por todo o processo de homologação do Java EE, e, por via de regra - mas não como verdade absoluta - são compatíveis). Para esclarecer, não estou pedindo para ninguém abrir mão das bibliotecas (por favor, reuso na cabeça!), apenas recomendo pesquisar se o próprio servidor de aplicação já não fornece uma versão da biblioteca desejada ou algo equivalente (e.g., Hibernate x Eclipselink para JPA).
      Não se preocupe em sempre incluir a versão bleedin edge de uma biblioteca no projeto. Geralmente as versões disponibilizadas pelo Application Server são mais do que suficientes (salvo exceções em lugares que ainda estão rodando Application Servers homologados para versões anteriores ao Java EE 5). Minha experiência nesse ponto é que quando você coloca uma biblioteca bleedin edge no projeto ela acaba permanecendo lá por mutos anos e eventualmente se torna legada (ninguém quer mexer em bibliotecas da aplicação por motivos óbvios, incluindo JAR Hells), enquanto que ao fazer referência as bibliotecas do App Server, a evolução vem lenta porém certamente conforme upgrades planejados para versões mais novas do container.
      Enquanto há bons argumentos contra depender do container (e.g., reduzir o trabalho de infraestrutura ao fazer deploy das bibliotecas; ter menos problemas ao fazer upgrade do servidor de aplicação e ter mais controle sobre o que está sendo utilizado no projeto), eu pessoalmente acho que é um trade-off válido.
      Eis um exemplo de como referenciar uma biblioteca no weblogic-application.xml (EAR).
      <wls:library-ref>
          <wls:library-name>jersey-bundle</wls:library-name>
          <wls:specification-version>1.1.1</wls:specification-version>
          <wls:implementation-version>1.1.5.1</wls:implementation-version>
      </wls:library-ref>
      Fico devendo exemplos de sobre como referenciar bibliotecas no JBoss AS, GlassFish, WebSphere, etc.

      Na verdade, como deixei muitos pontos em aberto nesse post (como escrever um ClassLoader, como exportar dependências do Maven e como referenciar bibliotecas nos principais servidores de aplicação), aceito sugestões sobre o tema de posts futuros. Por favor deixem seu comentário ou enviem uma mensagem através do formulário de contato.

      Um comentário:

      1. Há muito tempo que não lia um artigo interessante, bem feito e que realmente contribui. Parabens !!!

        ResponderExcluir