ant-dev mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From David Wood <obsid...@panix.com>
Subject Tasks, Taskdefs, classpaths, and ClassNotFound...
Date Tue, 23 Sep 2003 15:17:39 GMT
Hi everybody. I'm posting this to the lists in the hopes that the search
engines will pick it up and provide more help than I got. When I looked, I
saw lots of messages along the lines of "Help! Can't Define/change the
classpath within the ant build file"... but no good answers.

It seems some people were using or even defining their own custom Tasks,
only to find that "ClassNotFoundErrors" or "ClassCastExceptions" were
popping up as a result. If their experience was anything like mine, they
found that they could certainly define the classpath used to load their
custom task, but that once that task was loaded, it had problems finding
other classes it needed - even if they were within the classpath given in
the taskdef. A perplexing and frustruating problem.

The ugly solutions are obvious:

1) Using a script (or abusing your wrists) to specify a "system classpath"
for all of ant that includes the classes that your task can't find. This
is ugly, especially when not only is it unnecessary, but you're so close
to not needing to do it.

2) Putting all of the jars/classes that your task can't find into
ANT_HOME/lib. So now installation/transport of whatever you're doing is a
pain, and you've crudded up the ant namespace for every user of ant on
that system, potentially causing other problems.

I scoured google finding nothing of any substance about what exactly
caused this problem, or how it might be fixed. There were, however, a few
clues that led me to examine Cactus and JUnit, which have, in various
ways, dealt with issues of loading arbitrary classes in ant.

Ordinary Java operations like "new", casting literals, variable
declarations, and other "conventional" language features will operate via
the ClassLoader that loaded the Object they're running in. Even the
Reflection APIs appear to attempt to use the ClassLoader of the calling
Object. This, at least, makes some sense to me.

The problem comes with the way some Factory bootstrappers use reflection.
In these "bad" implementations, reflection will be used to find and
instantiate Factories, but with a "default" ClassLoader, rather than the
ClassLoader of the calling Object. I encountered this in code from Sun, in
the the JDOHelper class, which is used to bootstrap JDO
PersistenceManagerFactories. That method (by default) used the ClassLoader
of the current thread (Thread.currentThread().getContextClassLoader()).
Judging by other messages I found hunting around the net, this kind of
thing comes up in other situations as well.

If you defined a classpath for your taskdef, a special AntClassLoader was
created, configured with this classpath, and used to load your Task. But
inside your Task, when you, or anything you call, uses these "sloppy"
bootstrap routines, they can lose that ClassLoader (picking up ant's
instead), and that means that the custom classpath in your taskdef isn't
used.

Who knows what's affected. This may be rare, and I may be unlucky, but
bean stuff, factory stuff, database stuff, testing stuff, or anything else
that uses reflection internally could potentially have a problem. Not many
people write custom tasks that do these sorts of things, but it does come
up, and when it does, it seems to have left a trail of agony.

I found two ways to deal with this problem while making JDO work inside an
ant Task.

The sloppy way is to grab the "right" ClassLoader - the one you creataed
by giving a custom classpath in your taskdef - and shove it into the
current Thread. Yes, you can do that. FYI, these code examples have not
been compiled - they're just to give a general idea. So anywhere in the
Task (init(), execute()), but ideally right before the problem occurs:

  // Get the task class loader we used to load this tag.
  AntClassLoader taskloader = (AntClassLoader)
this.getClass().getClassLoader();

  // Shove it into the Thread, replacing the thread's ClassLoader:
  taskloader.setThreadContextLoader();

Then you do what you have to do. When your done, I highly advise doing a:

  // Reset the Thread's original ClassLoader.
  taskloader.resetThreadContextLoader();

Yes, taskloader remembers the original for later reset. This puts the
right ClassLoader where JDOHelper wants to look for it by default. I
tested this way cursorily and it seemed to work. I think it could be
useful if you're dealing with a series of similar problems rather than
just one instance.

However, it is messy. You don't want to change the ClassLoader except
where you need to. I don't know what else looks at the Thread's
ClassLoader... It might affect ant, it might affect other tasks, who
knows. However, if you're in a bind, you can try it - you might get away
with it. You do get some additional control with the AntClassLoader, which
can be useful for forcing a solution like this to work, via methods that
allow you to control which classes, or whole package roots, are handled by
the child and which are immediately delegated to the parent ("system")...
Anyway, another small indication of why going this way is potentially
troublesome.

The "targeted" way of doing this is what I settled on, because JDOHelper
was the only thing having a problem, and because it also supports
specifying a ClassLoader _other_ than the bad default for use in the
"getFactory" method in question. So instead of doing a
setThreadContextLoader(), you just pass it taskloader, and everything is
as it should be.

So that's how you can package custom Tasks using JDO (and anything else
that has this problem) without having to give complex install instructions
or write wrapper scripts.

-David Wood

---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@ant.apache.org
For additional commands, e-mail: dev-help@ant.apache.org


Mime
View raw message