4
votes

I'm trying to implement a custom classloader for tomcat. My first attempt yielded a class cast exception (apparently, tomcat tries to cast my loader to org.apache.catalina.loader.WebappLoader). Fine, I extended WebappLoader and added the catalina.jar to my buildpath.

Now I'm ready to deploy (I think). I'm getting this error:

SEVERE: Catalina.start: LifecycleException: start: : java.lang.NoClassDefFoundError: org/apache/catalina/loader/WebappLoader

Tomcat comes with catalina.jar to run, so I'm 99.9% sure it's already loaded into tomcat. I verified this by checking /server/lib/catalina.jar, which does contain the apache WebappLoader. Furthermore, manually linking another catalina.jar in creates a whole mess of problems, as expected.

I'm confused. Any tips would be hot.

Thanks!

Update: Interestingly, the same thing on tomcat6 (extending WebappLoader; worked on tomcat5.5), still causes a ClassCastException. Sounds to me like the class performing the cast was loaded using a different loader from the one which loaded my class. I don't see how I would have control over that anyway, unless maybe another missing tomcat config somewhere? Any ideas for tomcat6 either?

2

2 Answers

5
votes

Maybe I'm being dense, but I think that should be WebappClassLoader, not WebappLoader. The import looks ok though.

1
votes

Without knowing the specifics of your code & setup it's impossible to be sure, but it reminds me of a problem I encountered when trying my hand at custom classloaders.

The scenario is this:

  1. The boot loader (JVM proper/Tomcat/whichever) loads your code
  2. Your classloader loads your additions not available on the classpath of the above.
  3. Your code references those additions.
  4. Those additions are not available in the same namespace as the code loaded by the boot loader.
  5. Your code in the boot loader's namespace runs, attempts to reference code in your custom namespace and this is not visible from the boot loader's namespace. So at that point the JVM bails with the NoClassDefFound error.

The reason for this is that the classloader hierachy works in a single direction only: i.e. code from sub-namespaces (child classloaders) is not available (visible) in the broader parent namespace (parent classloader); and there is no good way to hack into this system.

Fortunately, you can define interfaces which are available in the parent namespace, then implement these in code only visible in the sub-namespace. Then, code that lives inside the parent namespace only is able to use this interface name for casting & method call purposes and thus access your sub-namespace component regardless.

In order for this to work, you must ensure that your custom classloader does not only load components that the parent loader has no access to (i.e. are outside of its classpath), but also those bits of your code which directly interfaces with these components and explicitly refer these symbols (typenames/methods etc.). Otherwise references to these components end up in the parent namespace (remember, the boot classloader loads all your own code by default) and you're back to the original problem.

You can do this by subverting the intended classloading delegation model. Normally, you would defer to the parent loader before attempting to load classes yourself. However, now you must check in advance that your code does not touch any of these components unvailable to the parent loader. The easiest way is probably to set up your code path in such a way that the classloader maintains a set of classnames which it is to load itself rather than allow the parent loader to load them.

You have to find a way to tell your custom classloader somehow, a type annotation on class declarations could be used for this. The idea here is that your classloader introspects classes loaded by the parent, and if it finds a particular custom annotation on the type name it will invoke methods on the annotation to get the name of the class symbols that it must not allow its parent loader to load.

Example:

@MyCustomAnnotation(implementation="my.custom.package.MyImpl")
public class MyFeatureProvider {
  public MyFeature getFeature() { // return an instance of MyImpl here }
}

Note that because the class MyFeatureProvider will be loaded before MyImpl is, your classloader will know in advance about the annotation in MyFeatureProvider so it will be able to override the default delegation model for MyImpl. Because the rest of your code only interacts with MyImpl as an instance of MyFeature the parent loader never need to balk at the sight of undefined symbols -- and the ClassNoDefFound error is solved.