This may not be too revolutionary, but I’ve spent enough time googling and asking questions on the Maven lists to believe that there isn’t a lot of information out there about topic so I thought I’d document it in a blog. Apologies to the Maven guys if this isn’t the best way of going about this (I believe Jason mentioned something about improvements to the Maven Embedder), but I needed a solution right now.
The situation was that I wanted to be able to decide at runtime which jars to put onto the execution classpath for the Jetty6 Maven Plugin. The decision is based on the runtime environment: if the user is running with < 1.5 JVM, then I need to be able to download and use the JSP 2.0 jars. If, however, the user is running with a 1.5 JVM, then the JSP 2.1 jars should be used (as these mandate JDK 1.5).
Rather than having to hard-code into my plugin a list of jars and their transitive dependencies for each version of JSP, I created one submodule for each JSP variant and listed all dependencies in the module’s pom.xml.
This reduced the problem to downloading and transitively resolving a pom on-the-fly, then getting all of the resolved artifacts on the plugin’s execution classpath.
Runtime Downloading and Transitive Resolution of a pom
I used the Maven tools for manipulating artifacts. You need to put some configuration parameter declarations into your plugin to gather the necessary factories etc from the runtime environment to drive the tools. The ones I used were:
/** * @component */ private ArtifactResolver artifactResolver; /** * * @component */ private ArtifactFactory artifactFactory; /** * * @component */ private ArtifactMetadataSource metadataSource; /** * * @parameter expression="${localRepository}" */ private ArtifactRepository localRepository; /** * * @parameter expression="${project.remoteArtifactRepositories}" */ private List remoteRepositories;
Then, it is a matter of downloading the pom, getting its dependencies and transitively resolving them. Here’s a snippet of the code I used to do the job generically:
public Set transitivelyResolvePomDependencies (MavenProjectBuilder projectBuilder, String groupId, String artifactId, String versionId, boolean resolveProjectArtifact) throws MalformedURLException, ProjectBuildingException, InvalidDependencyVersionException, ArtifactResolutionException, ArtifactNotFoundException { //get the pom as an Artifact Artifact pomArtifact = getPomArtifact(groupId, artifactId, versionId); //load the pom as a MavenProject MavenProject project = loadPomAsProject(projectBuilder, pomArtifact); //get all of the dependencies for the project List dependencies = project.getDependencies(); //make Artifacts of all the dependencies Set dependencyArtifacts = MavenMetadataSource.createArtifacts( artifactFactory, dependencies, null, null, null ); //not forgetting the Artifact of the project itself dependencyArtifacts.add(project.getArtifact()); List listeners = Collections.EMPTY_LIST; if (PluginLog.getLog().isDebugEnabled()) { listeners = new ArrayList(); listeners.add(new RuntimeResolutionListener()); } //resolve all dependencies transitively to obtain a comprehensive list of jars ArtifactResolutionResult result = artifactResolver.resolveTransitively(dependencyArtifacts, pomArtifact, Collections.EMPTY_MAP, localRepository, remoteRepositories, metadataSource, null, listeners); return result.getArtifacts(); }
Now we can make some environment-based decisions on which pom use to extract the artifacts we want:
//if we're running in a < 1.5 jvm Artifacts artifacts = resolver.transitivelyResolvePomDependencies(projectBuilder, "org.mortbay.jetty", "jsp-2.0", "6.0-SNAPSHOT", true); //else Artifacts artifacts = resolver.transitivelyResolvePomDependencies(projectBuilder, "org.mortbay.jetty", "jsp-2.1", "6.0-SNAPSHOT", true);
Having got the artifacts, now we need to place them on the execution classpath.
Runtime Maven Classpath Manipulation
This is the bit I found really hair-raising. I’m not convinced it’s a bullet-proof solution, but all testing to date seems to indicate its working fine.
Taking the Artifacts we got from the on-the-fly downloaded pom above, we need to put these into a Classloader and also arrange for the existing ContextClassLoader to be it’s parent (so we can resolve classes that are already on the plugin’s classpath).
The first solution that springs to mind is to put the urls of the download jars into a URLClassLoader and make the current ContextClassLoader it’s parent like this:
URL[] urls = new URL[artifacts.size()]; Iterator itor = runtimeArtifacts.iterator(); int i = 0; while (itor.hasNext()) urls[i++] = ((Artifact)itor.next()).getFile().toURL(); URLClassLoader cl = new URLClassLoader(urls, Thread.currentThread().getContextClassLoader()); Thread.currentThread().setContextClassLoader(cl);
However, after a lot of experimentation, it seems that this just simply does not work: the parent class loader does not seem able to correctly resolve classes and resource when delegated to from the URLClassLoader. The parent class loader is an instance of a ClassWorlds ClassLoader set up by the plugin execution environment.
Experimenting further, I discovered it is possible to create a new ClassWorlds classloading hierarchy, injecting the jars that we downloaded earlier, and linking the existing (ClassWorlds) classloader as the parent of the new hierarchy. It looks like this:
//create a new classloading space ClassWorld world = new ClassWorld(); //use the existing ContextClassLoader in a realm of the classloading space ClassRealm realm = world.newRealm("plugin.jetty.container", Thread.currentThread().getContextClassLoader()); //create another realm for just the jars we have downloaded on-the-fly and make //sure it is in a child-parent relationship with the current ContextClassLoader ClassRealm jspRealm = realm.createChildRealm("jsp"); //add all the jars we just downloaded to the new child realm Iterator itor = artifacts.iterator(); while (itor.hasNext()) jspRealm.addConstituent(((Artifact)itor.next()).getFile().toURL(); //make the child realm the ContextClassLoader Thread.currentThread().setContextClassLoader(jspRealm.getClassLoader());
When used this way, the parent ClassWorlds classloader is able to correctly resolve classes and resources. The Jetty6 Maven Plugin is therefore able to automatically provide the correct JSP version at runtime without necessitating any user configuration.
5 Comments
Anonymous · 02/07/2008 at 21:43
We got the following error related to what you descirbed
(instance of org/mortbay/jetty/webapp/WebAppClassLoader) of the current class,org/apache/cxf/jaxb/attachment/JAXBAttachmentUnmarshaller, and its superclass loader (instance of <bootloader>), have different Class objects for the type javax/activation/DataHandler used in the signature
Is it possible to have the maven plugin without the jetty-plus config and stuff running? Because when we run our war in a standalone jetty without jetty-plus everything works fine but when we use the maven jetty plugin we get the error above.
It seems that versions of the same library are loaded in different classloaders, nasty error.
Jan Bartel · 02/07/2008 at 23:23
Are you running jdk1.6? It sounds similar to this problem: http://jira.codehaus.org/browse/JETTY-420
When you run jetty standalone, are you also using jdk1.6 and have you removed the activation jar from the jetty lib? I’m trying to understand why it would work standalone and not for the plugin.
Anonymous · 03/07/2008 at 07:27
I am running with jdk 1.6.0_10-beta
It is very similar to the problem you stated. Therefor we thought it was a problem with the plugin as default running with jetty-plus jar in the classpath.
And we thought this was not the case when we ran standalone.
For the standalone version we use the same jdk (btw).
The theory we follow right now: because we use jdk 1.6 the activation.jar is already included in the jdk. And jetty 6.1 also includes it (because it was not included in jdk 5). So that is where the problem comes from.
The strange thing is we did not delete the activation.jar out of our standalone jetty setup. So the classpath seems to be different for the maven plugin and the standalone version.
If I run: "mvn -X jetty:run"
Get the following output
Other possibility: it is another dependency that messes things up. And then we are screwed.
We though maybe with the jetty 7 (pre2) it was maybe better because it is known that the activation.jar is in the jdk. But it gave the same error.
Jan Bartel · 25/07/2008 at 00:37
Hi,
The maven plugin does put jetty-plus on the classpath by default (with a transitive dependency on activation.jar), whereas jetty standalone does not by default include jetty-plus – you have to take extra actions to get those jars onto the runtime classpath.
You should be able to simply exclude the activation.jar from the plugin’s classpath.
I took a look at the output you linked to, and you seem to have excluded quite a lot of jars from the plugin’s classpath (possibly too many?).
In any case, the error seemed to be related to a non-existant directory rather than conflicting jar versions.
If you’d like to discuss this further, I suggest we move it over to the jetty mailing list (user@jetty.codehaus.org), where there are a more people who can help/benefit from the discussion.
cheers
Jan
M Lair · 20/08/2009 at 22:08
Hey Jan! Just wanted to say thanks for the post. I’ve been working through a Maven plug-in classpath issue for literally 3 days straight and was out of ideas when I stumbled upon this. And best of all — it worked! It made my day. Scratch that, it made my whole week. Thanks!
Comments are closed.