The Classpath is a Lie

Three years ago I wrote here about some frustrations and questions I had when trying to deal with the classpath and class loaders.

Back in May @Daniel_Szmulewicz blogged about this topic, finally answering a question I’ve had since then, where do all those extra DynamicClassLoader instances come from?

https://danielsz.github.io/blog/2021-05-12T13_24.html

Recently I’ve had reason once again to dive (too) deep into this topic. The result is a blog post of my own. It’s pretty heavy and technical, but I thought it was important to share some of this stuff for the next person who is trying to understand these ClassLoaders and how they relate to Clojure. I still feel like a noob with this stuff, but I’m a lot less confused than I was three years ago.

If you manage to make it through the Java snippets and technical details there’s a link at the end with some practical REPL helpers which you may find useful.

7 Likes

Loved it, really informative.

Sadly we can’t change the system classloader. Its classpath may as well be set in stone

Can’t you use -Xbootclasspath to change it :sunglasses:?

removing something that is part of the classpath the application started with is impossible

Seems it be possible to tombstone it though, like keep a list of classes for which in your custom getResource will pretend nothing was found and won’t call the parent methods. So getResource.coupd check in a tombstone-classes and if it finds it there, return nothing (or throw) or whatever it’s supposed to do when something is missing.

2 Likes

You’re right, I did consider that. It makes the logic a bit more complicated but worth a try.

The other thing that occured to me is that you could actually replace the application classloader. Sadly neither AppClassLoader nor its parent BuiltinClassLoader have public constructors. Using a URLClassLoader probably mostly works. Or have the DynamicClassLoader sit directly above the PlatformClassloader and have it handle the full classpath.

The question is always how this stuff interacts with other tools and libraries. On the Clojure side there are others who have come up with clever classloader hacks, and their assumptions about the state of the world may interfere with our own. Similarly on the Java side there’s a lot of variation on how people run their JVM. We did have one or two reports of people who had to disable the classpath hacking in Kaocha because it caused issues.

But I guess we can just try and see how far we get :slight_smile:

I am not all the way through your article yet, but so far it’s both enlightening and entertaining, thanks! :smiley:

1 Like

When I wanted to use add-lib in a shared PREPL, I asked Alex and ended up with something like this

(defn shared-prepl-server
  "Create a PREPL socket-server and return the port on which it is available"
  [opts]
  (let [socket-opts    (merge {:port   0
                               :name   "repl-node"
                               :accept 'clojure.core.server/io-prepl}
                              opts)
        current-thread (Thread/currentThread)
        cl             (.getContextClassLoader current-thread)
        _              (.setContextClassLoader current-thread (DynamicClassLoader. cl))
        socket         (start-server socket-opts)]
    {:socket socket
     :port   (.getLocalPort ^ServerSocket socket)}))

I have been using https://github.com/just-sultanov/clj-deps cos the tech was broken when I moved to Java 11 in the core library. Now that I see from your excellent blog post that it’s working again I’ll swap it back.

Great work as ever Arne!

I am just going to add that I the repl-repl never needed to change :local/root so that’s definitely a new trick :slight_smile: