-
Notifications
You must be signed in to change notification settings - Fork 25
Switching the ThreadContextClassLoader
This is something that pops up from time to time, when Java libraries don't take into account custom classloaders.
Usually there is a Java exception that resembles:
Caused by: java.lang.ClassCastException:
org.apache.xerces.jaxp.DocumentBuilderFactoryImpl cannot be cast to
javax.xml.parsers.DocumentBuilderFactory
Which is infinitely confusing. A good explanation of why this occurs, was stated on the javaloader-dev mailing list recently.
So this is where ClassLoaders in Java can bite you in the rear.. and can be a pain to debug if you don't understand how they work.
Strangely enough, I actually covered this in my cf.Objective() Advanced Java Integration talk, with exactly this library, and it could probably do with a good blog post too, because not a lot of CF'ers really get what happens here.
We've also talked about this a bit on this list as well, but it's a hard thing to explain without pictures.
Basically, you have to think of Classloaders (of which JL has one, and CF also has, well, a lot) like Shelves, but Shelves, that can turn around and say 'Hey, what you are looking for, is not on my shelve, maybe you should check the Shelf above me'.
So whenever you go get an instance of something, the code goes to the given ClassLoader, finds the reference to the Class it looks for and then creates an instance out of that Class definition. If the ClassLoader can't find it, it goes and asks its parent. What makes this whole experience fun, is that some ClassLoaders are "parent first" (Does my parent have it? If not, check me!), or "child first" (Do I have it, if not, check my parent), and you don't necessarily know which.
JavaLoader is Child First, just for reference.
For the issue you are getting, 2 rather painful things are happening:
- JavaLoader's ClassLoader parent will always point to the ColdFusion's System ClassLoader - which has a copy of dom4j, just like your JL library does. Normally this is no issue, as Jl is child first, so it doesn't hit the parent classloader, except...
- The ThreadContextClassLoader. The ThreadContextClassLoader is a ClassLoader that is stuck to the current Thread. It's usually responsible for loading whatever Classes you need to create your Java objects when you request them, unless you specifically use another ClassLoader (like JL's).
What makes this painful, is dom4j's implementation does some wacky stuff to get/set singletons via the ThreadContextClassLoader (which IMHO is a bad implementation, when considering multiple classloaders). So what happens is you go:
JLClassLoader - create dom4j
dom4j - hey, TCCL (ThreadContextClassLoader) - create me a DocumentBuilderFactoryImpl
dom4j - Let's use my new DocumentBuilderFactoryImpl
Java - wait a minute, you created a DocumentBuilderFactoryImpl via the TCCL, but you want to do stuff in JLClassLoader, which has a different DocumentBuilderFactoryImpl... GAH I don't know which is which!
Which is why you get this weird error:
Caused by: java.lang.ClassCastException: org.apache.xerces.jaxp. DocumentBuilderFactoryImpl cannot be cast to javax.xml.parsers.DocumentBuilderFactory
Basically because the two classes come from different ClassLoaders (which is a very bad thing), and it realises they are different, and can't make them the same.
So what do you do? Well, the cool thing is, you can switch out the TCCL at run time, to a different ClassLoader, and then switch it back after you are done. It isn't the cleanest thing in the world, and dom4j should really give you a way to tell it which classLoader to use (Spring does this), but you do what you have to.
The above approach works, but it unnecessary boilerplate code when you run into issues like above. So JavaLoader provides you with a nice way to switch the ThreadContextClassloader with it's own ClassLoader, and then switch it back to it's original when it is complete through the switchThreadContextClassLoader()
(link forthcoming) method.
The function can take three different method signatures to implement this in slightly different ways, depending on your need:
This allows you pass in an entire function to the function, and have it execute within a switched out ThreadContextClassLoader, and return the result.
For example, to call the custom function doFoo()
:
function doFoo() { /* does stuff */ }
result = javaloader.switchThreadContextClassLoader(doFoo);
By default, the classLoader parameter is the Javaloader classloader.
This calls a specific method by name on a specific object and returns the result, within a switch out ThreadContextClassLoader.
For example, to call the method bar()
on the object foo
:
var foo = new Foo();
result = javaloader.switchThreadContextClassLoader(foo, "bar");
By default, the classLoader parameter is the Javaloader classloader.
The method switchThreadContextClassLoader()
has been written so that it does not have any dependencies on any other methods inside of JavaLoader.
This means it can be mixed in to your ColdFusion page or Component, to be used as you need it to.
While you can still use the above two execution methods, there is another that is specifically suited to this:
This calls as specific method and returns the result within a switched out ThreadContextClassLoader. Since no object is defined, it does not need to be a public method on an object, and will simply execute within it's given context.
For example, to call the method doFoo()
after mixing in the switchThreadContextClassLoader()
method:
component
{
function init(required javaloader)
{
//mixin the function
variables.switchThreadContextClassLoader = javaloader.switchThreadContextClassLoader;
}
function doFoo() { /* does stuff */ }
function doFooSwitchedThreadContextClassLoader()
{
//call the mixin, and return result.
return switchThreadContextClassLoader("doFoo");
}
}
By default, the classLoader parameter is the Javaloader classloader.
It should be noted, that when you are switching out the ThreadContextClassLoader, you will generally want the ColdFusion classpath to be JavaLoader's classpath. See Class Loading for details on how to do that.