How do I control which ClassLoader loads a class?
Asked Answered
E

3

7

The situation at hand is not as simple as the title seems to indicate.

Java 1.6_17 running via JWS.

I have a class, lets say MyClass and one of its instance member variables is a Type from an errant 3rd party library where during class initialization it dynamically tries loading some of its own classes with Class.forName(String). In one of these cases it happens to dynamically call: Class.forName("foo/Bar").This class name doesn't follow the JLS for binary names and ultimately leads to a java.lang.NoClassDefFoundError: foo/Bar.

We have a custom ClassLoader which I've added a sanitize method to ClassLoader.findClass(String) and ClassLoader.loadClass(String) which fixes this problem.

I can call stuff like: myCustomClassLoader.findClass("foo/Bar")

Which then loads the class without any problems. But even if I load the class ahead of time, I still get the exception later. This is because during initialization of MyClass which refers to Bar - their code ends up calling Class.forName("foo/Bar") in a static block somewhere. This actually would be OK if the ClassLoader it was trying to use was my custom class loader. But it isn't. It is the com.sun.jnlp.JNLPClassLoader which doesn't do such sanitation, thus my problem.

I've made sure that Thread.currentThread().getContextClassLoader() is set to my custom class loader. But this (as you know) has no effect. I even set it as the first thing i do in main() due to some stuff I read and still, MyClass.class.getClassLoader() - is the JNLPClassLoader. If I could force it to NOT be the JNLPClassLoader and to use mine instead, problem solved.

How can I control which ClassLoader is used to load the class via their static Class.forName("foo/Bar") call made during class initialization? I believe if I can force MyClass.class.getClassLoader() to return my custom class loader, my problem will be resolved.

I'm open to other options if anyone has ideas.

TL;DR: Help me force all Class.forName(String) calls in a third party library which are referenced by MyClass - to use the classloader of my choosing.

Extrados answered 18/12, 2012 at 1:58 Comment(3)
"from an errant 3rd party library" The best strategy ultimately, is to replace that API.Menell
@AndrewThompson It may come to that. I am hoping it doesn't. So if anyone has any ideas - I'm all ears!Extrados
I did something similar by making a bootstrap class and using a URLClassLoader to load all of our external libraries from there. They aren't in the classpath when the boostrap class launches, so the only classloader they see is the custom one. Not an answer because I have no idea if it would work with JWS. We did it to load plugin libraries at runtime.Forswear
E
3

I think everyone gave good solid attempts at answering the problem. However, it turns out that I misdiagnosed the problem.

I had a coworker take over the problem and asked him to get a JDK with debug flags on so we could debug the JNLPClassLoader to see what was going on as I had tried all of the suggestions here + some.

We ended up getting OpenJDK because recompiling the JDK from scratch is a total nightmare (we tried). After getting OpenJDK working with our product and debugging through the JNLPClassLoader - it turns out that it was still using a REALLY old .jnlp from months earlier that had the resource path wrong and thus why it couldn't find the class.

We were confused why it was still using the ancient .jnlp even though we had redeployed the server correctly many times with the correct .jnlp and lots of code changes between which were reflected in our client application when run.

Well, it turns out that on client machines, Java caches the .jnlp file. Even if your application changes and it redownloads your application, it still won't re-download the new .jnlp for whatever reason. So it will use all of the new code, but look up resources/class paths using the cached .jnlp.

If you run: javaws -uninstall On the client machine then that will clear the .jnlp cache and next time it will use the correct .jnlp file.

Really sad that this was the problem. Hopefully, this saves someone else endless hours of frustration like it caused us.

Extrados answered 13/6, 2013 at 17:35 Comment(0)
R
5

This reminds me of an article I read 10 years ago about the classloading arrangements in Java. It's still there on JavaWorld.

The article won't answer your question directly, but it may help understand your problem. You need to cause MyClass to be loaded through your custom class loader and trump the default class loading behavior, which is to first delegate class loading to the parent classloader and only attempt to load a class if that fails.

Allowing MyClass to get loaded by a classloader other than yours will store a relationship from the instantiated class to that classloader (via getClassLoader) and cause Java to use that other classloader to try to discover any referenced classes found at compile time, effectively bypassing your custom class loader by virtue of the class loader hierarchy and the delegation model. If MyClass is instead defined by your class loader, you get a second chance.

It sounds like a job for something like URLClassLoader, overriding loadClass and trumping the delegation model for classes residing in your JARs. You'll probably want to use a bootstrap approach (as suggested by Thomas in a comment above) to force a single entrypoint class to be loaded through your custom class loader, dragging all the others with it.

Also informative is this other JavaWorld article by the same guy, which warns you about the caveats of Class.forName. That too may trip your classloading arrangements.

I hope this helps and proves informative. In any case, it sounds like a difficult solution that is easy to break as your code evolves.

Rockoon answered 18/12, 2012 at 4:31 Comment(2)
+1 Excellent information. I shall digest this tomorrow. If this pans out, I'll mark it as the answer. Thanks!Extrados
Where did your research net out? Any breaks?Rockoon
E
3

I think everyone gave good solid attempts at answering the problem. However, it turns out that I misdiagnosed the problem.

I had a coworker take over the problem and asked him to get a JDK with debug flags on so we could debug the JNLPClassLoader to see what was going on as I had tried all of the suggestions here + some.

We ended up getting OpenJDK because recompiling the JDK from scratch is a total nightmare (we tried). After getting OpenJDK working with our product and debugging through the JNLPClassLoader - it turns out that it was still using a REALLY old .jnlp from months earlier that had the resource path wrong and thus why it couldn't find the class.

We were confused why it was still using the ancient .jnlp even though we had redeployed the server correctly many times with the correct .jnlp and lots of code changes between which were reflected in our client application when run.

Well, it turns out that on client machines, Java caches the .jnlp file. Even if your application changes and it redownloads your application, it still won't re-download the new .jnlp for whatever reason. So it will use all of the new code, but look up resources/class paths using the cached .jnlp.

If you run: javaws -uninstall On the client machine then that will clear the .jnlp cache and next time it will use the correct .jnlp file.

Really sad that this was the problem. Hopefully, this saves someone else endless hours of frustration like it caused us.

Extrados answered 13/6, 2013 at 17:35 Comment(0)
J
2

If you run out of ideas with patching the ClassLoaders themselves, you might consider rewriting the library bytecode itself -- just replace the "foo/bar" constant with the correct value, and then you don't need to customize further class loading at all!

You could do this either at runtime or beforehand.

Jopa answered 18/12, 2012 at 5:25 Comment(1)
+1 Excellent idea. However as I noted in my question (and tried to make clear but failed - sorry!) it is doing dynamic class loading. I grabbed the source code already and grepped for "foo/bar" and all that came up were imports (and class def itself). So the class name isn't static. This problem happened about a year ago with this exact library. It was using our custom classloader, and after debugging through it I saw it was calling Class.forName() with dynamic strings and so I modified our custom classloader to sanitize the class name and that fixed the problem until this happened.Extrados

© 2022 - 2024 — McMap. All rights reserved.