Java, Classpath, Classloading => Várias versões do mesmo jar / projeto

117

Eu sei que essa pode ser uma pergunta boba para programadores experientes. Mas eu tenho uma biblioteca (um cliente http) que alguns dos outros frameworks / jars usados ​​em meu projeto requerem. Mas todos eles exigem diferentes versões principais, como:

httpclient-v1.jar => Required by cralwer.jar
httpclient-v2.jar => Required by restapi.jar
httpclient-v3.jar => required by foobar.jar

O carregador de classe é inteligente o suficiente para separá-los de alguma forma? Mais provável que não? Como o Classloader lida com isso, caso uma Classe seja a mesma em todos os três jars. Qual está carregado e por quê?

O Classloader só pega exatamente um jar ou mistura classes arbitrariamente? Portanto, por exemplo, se uma classe for carregada da versão 1.jar, todas as outras classes carregadas do mesmo carregador de classe irão para o mesmo jar?

Como você lida com esse problema?

Existe algum truque para de alguma forma "incorporar" os frascos no "required.jar" de forma que eles sejam vistos como "uma unidade / pacote" pelo Classloader, ou de alguma forma vinculados?

jens
fonte

Respostas:

56

Os problemas relacionados ao Classloader são um assunto bastante complexo. Em qualquer caso, você deve ter em mente alguns fatos:

  • Os carregadores de classe em um aplicativo geralmente são mais de um. O carregador de classes bootstrap delega para o apropriado. Quando você instancia uma nova classe, o carregador de classe mais específico é chamado. Se ele não encontrar uma referência para a classe que você está tentando carregar, ele delega para seu pai, e assim por diante, até que você chegue ao carregador de classes de bootstrap. Se nenhum deles encontrar uma referência à classe que você está tentando carregar, você receberá uma ClassNotFoundException.

  • Se você tiver duas classes com o mesmo nome binário, pesquisáveis ​​pelo mesmo carregador de classe, e quiser saber qual delas está carregando, você só pode inspecionar a maneira como aquele carregador de classe específico tenta resolver um nome de classe.

  • De acordo com a especificação da linguagem java, não há uma restrição de exclusividade para um nome binário de classe, mas, pelo que posso ver, deve ser exclusivo para cada carregador de classe.

Posso descobrir uma maneira de carregar duas classes com o mesmo nome binário, e isso envolve carregá-las (e todas as suas dependências) por dois carregadores de classe diferentes substituindo o comportamento padrão. Um exemplo aproximado:

    ClassLoader loaderA = new MyClassLoader(libPathOne);
    ClassLoader loaderB = new MyClassLoader(libPathTwo);
    Object1 obj1 = loaderA.loadClass("first.class.binary.name", true)
    Object2 obj2 = loaderB.loadClass("second.class.binary.name", true);

Sempre achei a customização do classloader uma tarefa complicada. Eu prefiro sugerir evitar várias dependências incompatíveis, se possível.

Luca Putzu
fonte
13
O carregador de classes bootstrap delega para o apropriado. Quando você instancia uma nova classe, o carregador de classe mais específico é chamado. Se ele não encontrar uma referência para a classe que você está tentando carregar, ele delega para seu pai. Por favor, tenha paciência comigo, mas depende da política do carregador de classe que é, por padrão, o primeiro pai. Em outras palavras, a classe filha primeiro pedirá a seu pai para carregar a classe e carregará apenas se toda a hierarquia falhar ao carregá-la, não ??
deckingraj
5
Não - normalmente, um carregador de classe delega a seu pai antes de procurar a própria classe. Veja o javadoc da classe para Classloader.
Joe Kearney
1
Acho que o tomcat faz isso da maneira descrita aqui, mas a delegação "convencional" é pedir primeiro ao pai
rogerdpack
@deckingraj: depois de pesquisar no Google, encontrei isso na documentação do oracle: "No design de delegação, um carregador de classe delega o carregamento de classe para seu pai antes de tentar carregar uma classe em si. [...] Se o carregador de classe pai não pode carregar uma classe, o carregador de classes tenta carregar a própria classe. Na verdade, um carregador de classes é responsável por carregar apenas as classes não disponíveis para o pai ". Vou investigar mais a fundo. Se isso surgir como a implementação padrão, atualizarei a resposta de acordo. ( docs.oracle.com/cd/E19501-01/819-3659/beadf/index.html )
Luca Putzu
20

Cada carga de classe escolhe exatamente uma classe. Normalmente, o primeiro encontrado.

OSGi tem como objetivo resolver o problema de várias versões do mesmo jar. Equinox e Apache Felix são as implementações de código aberto comuns para OSGi.

Tarlog
fonte
6

O Classloader carregará as classes do jar que estava no classpath primeiro. Normalmente, versões incompatíveis da biblioteca terão diferenças nos pacotes, mas no caso improvável, eles são realmente incompatíveis e não podem ser substituídos por um - tente jarjar.

Alex Gitelman
fonte
6

Classloaders carregam classe sob demanda. Isso significa que a classe exigida primeiro por seu aplicativo e as bibliotecas relacionadas seriam carregadas antes de outras classes; a solicitação para carregar as classes dependentes é normalmente emitida durante o processo de carregamento e vinculação de uma classe dependente.

É provável que você encontre LinkageErrors que indicam que definições de classe duplicadas foram encontradas para carregadores de classe. Normalmente, não tentam determinar qual classe deve ser carregada primeiro (se houver duas ou mais classes com o mesmo nome presentes no caminho de classe do carregador). Às vezes, o carregador de classe carrega a primeira classe que ocorre no caminho de classe e ignora as classes duplicadas, mas isso depende da implementação do carregador.

A prática recomendada para resolver esse tipo de erro é utilizar um carregador de classe separado para cada conjunto de bibliotecas que possuem dependências conflitantes. Dessa forma, se um carregador de classe tentar carregar classes de uma biblioteca, as classes dependentes serão carregadas pelo mesmo carregador de classe que não tem acesso às outras bibliotecas e dependências.

Vineet Reynolds
fonte
1

Você pode usar o URLClassLoaderfor require para carregar as classes de uma versão diff-2 de jars:

URLClassLoader loader1 = new URLClassLoader(new URL[] {new File("httpclient-v1.jar").toURL()}, Thread.currentThread().getContextClassLoader());
URLClassLoader loader2 = new URLClassLoader(new URL[] {new File("httpclient-v2.jar").toURL()}, Thread.currentThread().getContextClassLoader());

Class<?> c1 = loader1.loadClass("com.abc.Hello");

Class<?> c2 = loader2.loadClass("com.abc.Hello");

BaseInterface i1 = (BaseInterface) c1.newInstance();

BaseInterface i2 = (BaseInterface) c2.newInstance();
Pankaj Kalra
fonte