47

I have read a lot about Java classloaders, but so far I have failed to find an answer for this simple question:

I have two versions of com.abc.Hello.class in jars v1.jar and v2.jar. I want to use both in my application. What is the simplest way of doing this ?

I don't expect to be that simple, but something along these lines would be awesome :

Classloader myClassLoader = [magic that includes v1.jar and ignores v2.jar]
Hello hello = myclassLoader.load[com.abc.Hello]

And in a different class :

Classloader myClassLoader = [magic that includes v2.jar and ignores v1.jar]
Hello hello = myclassLoader.load[com.abc.Hello]

I would like to avoid using OSGi.

kms333
  • 3,147
  • 7
  • 31
  • 39
  • 3
    Personally, I don't think I would want such magic as it would lead to a maintenance/debugging nightmare... could you elaborate why you need this? – beny23 Aug 01 '12 at 12:42
  • 6
    Multiple plugins which depend on different versions of the same shared library would be a reason for this. – Parker Kemp Dec 09 '16 at 18:17
  • 1
    @beny23 you haven't heard of jar hell? I just hit it again. selenium is using one version and google component a different version of guava and selenium is failing so I need selenium to use the jar it was tested with and google component to use the version it was tested with which are different. – Dean Hiller Feb 26 '20 at 13:50

4 Answers4

54

You're going the right way. You must take some things into account.

The normal thing is classes that exist in parent classloaders are used. So if you want two versions those classes must not be there.

But if you want to interact you can use reflection, or even better a common interface. So I'll do this:

common.jar:
BaseInterface

v1.jar:
SomeImplementation implements BaseInterface

v2.jar:
OtherImplementation implements BaseInterface

command-line:
java -classpath common.jar YourMainClass
// you don't put v1 nor v2 into the parent classloader classpath

Then in your program:

loader1 = new URLClassLoader(new URL[] {new File("v1.jar").toURL()}, Thread.currentThread().getContextClassLoader());
loader2 = new URLClassLoader(new URL[] {new File("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();
pelumi
  • 1,530
  • 12
  • 21
helios
  • 13,574
  • 2
  • 45
  • 55
  • 3
    upvoted because of the explanation about NOT having the two versions in the main classpath. It would be helpful if you could also explain how they'd use c1 and c2 in code, and how that would compile. That is, how do they invoke `c1.someMethod()`? – CPerkins Aug 01 '12 at 12:50
  • Thanks! One more detail: "common" classes can be passed along main code and v1/v2 code. common classes are classes loaded by parent classloader so they are available to any of them (parent, v1 based and v2 based). All JRE classes fall into this category. That's why you can pass java.lang.String along methods by example. – helios Aug 01 '12 at 12:52
  • @CPerkins: well, maybe I've made it implicit. Anyway: you always use BaseInterface, and it's present in parent class loader and because of that, available from main code (as String is, or any other JRE class). You never use from main code classes loaded only in the child classloaders. Bottom-line: BaseInterface must have the methods you'll need from main code. – helios Aug 08 '12 at 07:35
  • 1
    I found the code above to always load the first class in the classpath, despite asking it to load from two different jars. The reason for this is probably class loader parent delagation. By setting the parent to null, I was able to load the same class from two different jars: loader1 = new URLClassLoader(new URL[] {new File("v1.jar").toURL()}, null); loader2 = new URLClassLoader(new URL[] {new File("v2.jar").toURL()}, null); – Ali Cheaito Aug 06 '14 at 14:24
  • 2
    @helios I have implemented your proposal to handle non-backward compatible version of elasticsearch client usage. Checkout https://github.com/atulsm/ElasticsearchClassLoader – Atul Soman Feb 13 '16 at 19:23
  • I have the same problem but what if they don't implement the same interface. as the library code is not available to me. so I cannot add the interface implementation myself. so I cannot cast to my desired version of the class. right? – mostafa.S Feb 06 '18 at 11:18
  • @mostafa.S I didn't try, but you should be able to use `Object` as interface. Then, in general, how do you expect to use the loaded classes? The api you will use is the common interface you are looking for. – Juh_ Sep 20 '19 at 15:40
9

You have almost written the solution. I hope the following code fragment will help.

ClassLoader cl = new URLClassLoader(new URL[] {new File("v1.jar").toURL()}, Thread.currentThread().getContextClassLoader());
Class<?> clazz = cl.loadClass("Hello");

Replace v1.jar by v2.jar and this code will load version #2.

AlexR
  • 114,158
  • 16
  • 130
  • 208
2

For a sample implementation of the accepted answer by @helios checkout github.com/atulsm/ElasticsearchClassLoader

Daniel Hári
  • 7,254
  • 5
  • 39
  • 54
Atul Soman
  • 4,612
  • 4
  • 30
  • 45
  • This example seems have some problems: 1: It throws exception: "org.elasticsearch.client.transport.NoNodeAvailableException: None of the configured nodes are available", 2: There is a "CustomClassLoader" that is seems to be not used at all. A good example should be a working case without useless additional misleading information... – Daniel Hári Jun 02 '16 at 21:06
0

it depends on what are u going to do with both versions and how, but in general you can at least load 2 version of class in different classloaders, and then set the Thread.contextClassloader() and play...

see http://www.javaworld.com/javaqa/2003-06/01-qa-0606-load.html and http://docs.oracle.com/javase/jndi/tutorial/beyond/misc/classloader.html

Andrey Borisov
  • 3,160
  • 18
  • 18