Does anyone know the classloader wizardry required to use external third-party jars as "provided" at runtime?
I already posted about this in the platform dev Slack group and had a quick chat with one person there who has solved this for Clojure, and I've also already implemented a fully-working solution by dynamically adding jar URLs to the existing UrlClassLoader. However, I'm still not satisfied and hope there's a better, more encapsulated solution available.
Basically I've integrated a third-party Java-based tool into my custom plugin, but for a number of reasons I don't actually bundle that tool's jars. I do include them in the local plugin development environment as a "provided" library so that I get static, compile-time validation against the classes from those jars, but the code that works against those classes only comes into play if the end user has configured my plugin against a local distribution of the third-party tool from which I add the appropriate subset of jars to the runtime class path. As I said, right now I'm doing that to the existing IDE class loader via UrlClassLoader#addFiles, and if I see that the configured third-party tool has changed to another distribution, I tell the user that they'll need to restart the IDE to use the new distribution. Again, it works, and to be fair, it works really well...but it feels wrong.
I've tried to get this working with a completely distinct class loader so that it's fully-isolated from the IDE's own class loader, but so far I haven't figured out the right magic incantation to make it work. I've tried using a URLClassLoader with the IDE's class loader as its parent, overriding loadClass to apply a load-local-first strategy, but then I end up with conflicting versions of the same classes. And of course I've tried using ClassLoaderUtil#computeWithClassLoader so that the context class loader is set to my custom class loader during execution, but again, no go.
Obviously at some point I may have to decide that what I have works and I need to stop spinning my wheels on this, but before I do so, I wanted to check with one more resource--this community--first. Any insights you can share are greatly appreciated!
Please sign in to leave a comment.
It is easy. See example https://github.com/JetBrains/intellij-community/blob/6b1e814e93698fc89ecf580494c838b6f11267b5/platform/build-scripts/groovy/org/jetbrains/intellij/build/impl/BuildHelper.groovy#L511 (it is groovy, but doesn't matter, same applies for production).
Thanks much. Overall this looks very similar to what I've been trying.
It looks like in the end, though, the resulting class loader is being used in a purely-reflective manner. In the BuildHelper constructor, I see it using reflection to populate multiple MethodHandle objects which just seem to be reflected Method wrappers for dynamic invocation. Is that in fact the case? If so, I'm hoping for a solution that allows me to have static compile-time references to types and other symbols from the dynamically-loaded jars as I can when I extend the IDE's existing UrlClassLoader, but perhaps that's not realistic?
Thanks again for taking the time to answer and providing the referenced example!
Okay, I mostly have it working. The only issue now is where types are used by both the standard IDE class loader and the custom class loader, e.g., one caches a value and the other retrieves that cached value. I've disabled that cache for the moment and now things actually seem to work. I'm going to try to have all access to that cache occur through the custom class loader as well. Thanks again for the help!
I don't like this forum, as you have already used the platform dev Slack group, let's use — I created https://jetbrains-platform.slack.com/archives/C5U8BM1MK/p1636561931285000?thread_ts=1636561919.284900&cid=C5U8BM1MK.