HOW-IT-WORKS: Running JAXB/WS 2.1 tools on JDK 6
This is a follow up to my previous post.
A reader asked how my "danegrous classloader trick" works, and another reader asked if he can use this technique to run his applications (with JAXB 2.1) on JavaSE 6. I've got a few internal people who had the same question, so I'm going to explain how it works today.
The following picture illustrates the basic design:
When our code gets the control of the execution for the first time, the "surrounding environment" already creates a few classloaders for us. The surrounding environment is Ant when we are invoked as an Ant task, and it's Java launcher (the code that invokes your main method) when we are invoked as a CLI tool.
You can't really assume too much about how those classloaders are set up, but generally speaking it does the following:
- It can load JAXB API classes. It could be either 2.0 (like if the user runs Ant on SE6), 2.1, or a mixture.
- It can load JAXB RI classes (otherwise our code will never gets executed in the first place)
- For all the classes that it can load, it can also locate .class files as resources.
In the above picture, these existing classloaders correspond to the bootstrap classloader and the cloud on the right. It's a cloud because we really shouldn't be assuming too much about the structure of these classloaders. The third assumption is not necessarily true (and that's one of the reasons why this is dangerous), although it's true in a reasonable environment like Ant, Maven, containers, etc. We need to make this assumption for this scheme to work.
The first thing we check is if this classloader can load everything we need correctly. So we first check if it's loading JAXB API 2.1. Doing this requires us to find a class that has been around since 2.0 (or better yet 1.0), with a method newly added in 2.1. We can't just check the existence of a class newly added in 2.1, because that would fail to diagnose the situation correctly if 2.0 API is in the bootstrap but 2.1 API is in other classloaders.
If this check detected that our environment is already loading JAXB API 2.1 for us, then there's nothing else needed. We just proceed with the rest of processing.
But if this check detected that our environment is loading 2.0 API, then the trick starts. First, we need to create a classloader that selectively disables delegation to the parent classloader (see Masking CL above.) Such a classloader allows another copy of JAXB API to be loaded below. This masking classloader needs to hide JAXB API and everything that depends on it (like all the RI code) — for example, if you let the RI loaded by the existing environment, then those classes would end up seeing the JAXB API in the existing environment, not the JAXB API below the masked classloader.
Then we create an URLClassLoader beneath it, by specifying the jar files of the JAXB 2.1 APIs. We are assuming that the JAXB 2.1 API class files are available somewhere in the envronment (like in the application classloader if this is CLI, or some AntClassLoader if this is Ant.) They are just not loaded because 2.0 API in the upper classloader takes precedence. To find out where those 2.1 jars are, we do ClassLoader.getResource("javax/xml/bind/annotation/XmlSeeAlso.class") (XmlSeeAlso is a new class in 2.1.) This likely returns a jar URL like jar:file://path/to/jaxb-api-2.1.jar!javax/xml/bind/annotation/XmlSeeAlso.class, so from there inferring the actual API jar URL is easy (and this is another reason why this is dangerous; URL need not be a jar URL, although they normally are.)
And now that we have a classloader that can load JAXB API 2.1 without interference from the existing environment, we create another classloader (see ParallelWorld CL above) to load the rest of the RI. For this we don't really need to locate where those jar files are. We can simply ask the existing environment to find the class files for us. All this classloader needs to do is to load the classes in this classloader, instead of delegating the loading to the parents. Got it?
At this point the end result is that if we use the parallel world classloader, it can load all 2.1 APIs and RIs correctly. So we finally move the execution to the entry point inside this classloader. Phew.
Can you use this for your app?
The same technique can be used by your application, yes. If you think about it, the JAXB RI is just a bunch of code that relies on the JAXB 2.1 API, so in that sense it's the same as your code.
The tricky part is that this technique ends up loading two sets of classes. The existing environment can most likely load your application classes, and this parallel classloader will load them again, too. Even though they are of exact same definition, as far as JVM is concerned they are different classes.
So passing values between the code that lives outside in the existing environment and the code that lives inside the parallel classloader becomes problematic. In case of XJC, the only thing being passed around is String (for CLI) and XML infoset (for Ant tasks), which are both defined outside XJC. So this was a non-issue.
But if types defined in your code are passed around, then you need to set the masking right so that you'll only get one copy of those classes. That means those classes won't be able to have any reference to the JAXB API.
This scheme also relies on the assumption that masking classloader can block the access to the bootstrap classloader, so it's likely to fail in more complicated classloader set up like classworlds or IDE plugins, where classloaders don't form a tree structure.
Anyway, this is how it works. I'm sorry for a lengthy post, but I didn't have time to write a shorter one! The implementation of these classloaders can be seen in istack-commons