Adding paths to your class loader

8:52 AM , , 0 Comments

So, a class loader is immutable in Java, right? At least, there are no setter methods in the public API (except the assert stuff) and there is no obvious way to specify what class loader you might want to use where.

This came to bug me while creating a maven plugin the other day in which the plugin needed to read a specific classpath resource from the project it was running in. Since maven plugins run in their own class loader, I wasn't going to be able to access project classpath resources.

I might have been able to add the @requiresDependencyResolution metadata annotation to resolve the problem, but we really didn't want to box ourselves in to needing an enclosing project to run the plug-in.

The Maven Exec Plugin gave me an idea.

The Maven Exec Plugin is the maven-y way to run command-line java through a maven goal. It can either be run within the maven process as a separate thread or as a separate process. Either way, it has the same challenge of propagating the enclosing project's classpath on to a separate and distinct classpath context.

In the in-maven-process case, what they do is create their own classloader, and then run the invocation of the main method inside a separate thread, setting that threads context classloader along the way:



private void executeWithClassLoader(Runnable runnable, ClassLoader classLoader) throws MojoExecutionException {
IsolatedThreadGroup threadGroup = new IsolatedThreadGroup( runnable.getClass().getName() );

Thread bootstrapThread = new Thread( threadGroup, runnable, runnable.getClass().getName() + ".run()" );
bootstrapThread.setContextClassLoader(classLoader);
bootstrapThread.start();

try {
bootstrapThread.join();
} catch ( InterruptedException e ) {
Thread.currentThread().interrupt(); // good practice if don't throw
getLog().warn( "interrupted while joining against thread " + bootstrapThread, e ); // not expected!
}

synchronized ( threadGroup )
{
if ( threadGroup.uncaughtException != null )
{
throw new MojoExecutionException( "An exception occured while executing the Java class. "
+ threadGroup.uncaughtException.getMessage(),
threadGroup.uncaughtException );
}
}
}


The Runnable that is passed in is the section of code where you actually need access to the project's classpath. The ClassLoader is created like this:

URL outputDirectory = new File( project.getBuild().getOutputDirectory() ).toURI().toURL();
ClassLoader classLoader = new URLClassLoader( new URL[] { outputDirectory } );
this.executeWithClassLoader(runnable, classLoader);

Of course, there may be other directories or artifacts that you need to add for your runnable to function correctly, but that is the basic idea.

For more ideas, check out http://svn.codehaus.org/mojo/tags/exec-maven-plugin-1.2/src/main/java/org/codehaus/mojo/exec/ExecJavaMojo.java

Josh Cummings

"I love to teach, as a painter loves to paint, as a singer loves to sing, as a musician loves to play" - William Lyon Phelps

0 comments: