Visitors are also reading...
← PreviousNext →

Java classpath hell solution

16 Aug 2016

If there's one thing I really like in nodejs is how it loads dependencies.
Each dependency is loaded relatively from the current file or from a known path (usually node_modules).
Unfortunately in java things are much messier. Today I will show how you can create a folder named java_modules that acts similarly to node_modules.

The example

In today's post we will consider the following as the example at hand:

package guymograbi;

public class MyMessage {
    public String getMessage(){
        return "message from dep_1";
    }
}

The problem we are trying to solve is about multiple classes named guymograbi.MyMessage in the classpath.
What is I have module-a.jar and module-b.jar (which can represent different versions of the same jar, right?), each containing guymograbi.MyMessage.

What if we have dependency-a and dependency-b both relying on module-a and module-b respectively?
Lets also assumes there's no backward compatibility.
We need to find some way to let dependency-a load module-a , while letting dependency-b load module-b and avoid collisions.

Turns out the solution is not that hard or crazy.. We might want to consider changing the way we pack our project in java.

First solution - Use reflection

We will soon see that this solution is not so good. We are actually losing all of java's benefits.
But this is how it would look like should we choose to use this method

public String runGetMessageFromDep1() throws MalformedURLException, ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
    URL url = new File("java_modules/dep_1.jar").toURI().toURL();
    URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{url});
    Object o = urlClassLoader.loadClass("guymograbi.MyMessage").newInstance();
    return (String) o.getClass().getMethod("getMessage").invoke(o);
}

Ug-ly!

Do I even need to specify the reasons why not to use this method?

Note! It would be better to find the jar by using getResource rather than File

This is a good point to bring up now, and this will be very useful below when we try to resolve problems to scale this solution.

If we could assume that java_modules is in our classpath, all we need to do is

return MyMain.class.getClassLoader().getResource("dep_1.jar").toURI().toURL();

This is even more similar to how node_modules behave in nodejs. Which for me looks like a good sign.

Second solution - ServiceLoader!

This solution assumes there's an API exposed in an external jar, which is quite reasonable nowadays. So ServiceLoader assumes each interface has a declared implementation. In our example it is :

resource/META-INF/services
                    +---- guymograbi.IMyMessage

And the file's content is the implementation classname - guymograbi.MyMessage.
This is similar to package.json main entry.
Still this is a bit ugly.. can we avoid this?
For now, this is how the code looks like:

// .. create class loader like before
ServiceLoader<IMyMessage> load = ServiceLoader.load(IMyMessage.class, urlClassLoader);
for (IMyMessage t : load) {
    return t;
}
throw new RuntimeException("could not find implementation to IMyMessage");

Using both worlds

Do we really need the ServiceLoader?
Why not simply use:

return (IMyMessage) urlClassLoader.loadClass("guymograbi.MyMessage").newInstance();

Now this is becoming more like nodejs. We are requiring classes relative to their classpath.
Now there's no need to declare all interfaces and their implementations in META-INF.

Piece of cake

Do you use any factory? Spring, Guice?
Then you can simply implement a factory that uses ServiceLoader and you will never know the difference :)
You will get the same behavior but a cleaner classpath.

So what's stopping me from using it right now?

Better way to pack my project

Before I use it, there is 1 more thing I am missing.. An easy way to pack the project.
I would like to have the following structure

   - my-jar.jar
   + java_modules // a directory that contains all my dependencies but is not included in the classpath
       - dependency #1
       - etc..

While this problem exists, it is less interesting.

I need this solution to scale

If I use ServiceLoader in my module as described, I need a way to make my module reused.
Basically this means I need the first problem to scale.

This is a much more interesting problem to solve.

If everyone were to use ServiceLoader tomorrow it would be easier, but that's not the case.

So we need

How to solve those problems

← PreviousNext →