3
votes

In order to reduce dependencies on external libraries (and mostly as a learning exercise), I've decided to add a ServletContextListener to a pedagogical webapp I'm developing. It will build up a registry of class names (stored as Strings) that have a certain annotation by scanning the "WEB-INF/classes" directory.

As part of this, I've written a custom ClassLoader that I can throw away every so often to prevent filling the permgen when a context is loaded by abusing the WebappClassLoader that I start with.

Unfortunately, I'm getting a nasty java.lang.NoClassDefFoundError: javax/websocket/server/ServerEndpointConfig$Configurator exception when it's attempting to load one of my ServerEndpointConfigurators.

public class MetascanClassLoader extends ClassLoader
{
    private final String myBaseDir;

    public MetascanClassLoader( final String baseDir )
    {
        if( !baseDir.endsWith( File.separator ) )
        {
            myBaseDir = baseDir + File.separator;
        }
        else
        {
            myBaseDir = baseDir;
        }
    }

    @Override
    protected Class<?> loadClass( final String name, final boolean resolve )
    throws ClassNotFoundException
    {
        synchronized( getClassLoadingLock( name ) )
        {
            Class<?> clazz = findLoadedClass( name );
            if (clazz == null)
            {
                try
                {
                    final byte[] classBytes =
                        getClassBytesByName( name );
                    clazz = defineClass(
                        name, classBytes, 0, classBytes.length );
                }
                catch (final ClassNotFoundException e)
                {
                    if ( getParent() != null )
                    {
                        clazz = getParent().loadClass( name );
                    }
                    else
                    {
                        throw new ClassNotFoundException(
                            "Could not load class from MetascanClassloader's " +
                                "parent classloader",
                            e );
                    }
                }
            }
            if( resolve )
            {
                resolveClass( clazz );
            }
            return clazz;
        }
    }

    private byte[] getClassBytesByName( final String name )
    throws ClassNotFoundException
    {
        final String pathToClass =
            myBaseDir + name.replace(
                '.', File.separatorChar ) + ".class";
        final ByteArrayOutputStream baos = new ByteArrayOutputStream();
        try( final InputStream stream = new FileInputStream( pathToClass ) )
        {
            int b;
            while( ( b = stream.read() ) != -1 )
            {
               baos.write( b );
            }
        }
        catch( final FileNotFoundException e )
        {
            throw new ClassNotFoundException(
                "Could not load class in MetascanClassloader.", e );
        }
        catch( final IOException e )
        {
            throw new RuntimeException( e );
        }
        return baos.toByteArray();
    }
}

Some of the code was influenced by the default ClassLoader implementation.

Here's the general flow of what I'm attempting to do:

  1. Grab a lock for the class name I'm trying to load to make sure other threads (if I attempt to hit this ClassLoader in parallel execution at some point in the future) don't try to load the same class twice.
  2. Check to see if i'm already loaded this class using findLoadedclass();
  3. If it's not already loaded, attempt to pull in the class from my "WEB-INF/classes" directory.
  4. If this fails, delegate to the parent classloader.
  5. If this still fails, blow up (throw).

I can guarantee that I'm passing in class names correctly after scanning for class files - this works perfectly if I replace MetascanClassLoader.load( className ) calls with Class.forName( className ), but as mentioned earlier, I don't want to hammer the permgen.

It appears that it only falls over when trying to load classes that contain a reference to classes that can only be found packaged with Tomcat. It has no problems with Java SE classes at all.

Let me know if you also happen to notice anything particularly insidious / distasteful that I'm doing other than causing it not to work.

UPDATE: it appears that using the default constructor for the superclass sets the parent classloader to the system classloader, which would explain the missing classes found in Tomcat.

I added the following line as the first line in my constructor:

super( Thread.currentThread().getContextClassLoader() );

Unfortunately, I'm still running into problems as all the Class objects that are returned by my ClassLoader are now empty, with no information except for the class name. (I inspected the internal fields using Eclipse to discover this.)

MORE INFO: Java SE classes are still being loaded correctly, by object inspection. When I inspect one of the classes that lives in WEB-INF\classes, this is the sort of behaviour I get when I try to inspect the class object at any point after loadClass() is called (I've hidden the package names to prevent sharing unnecessary project info). Eclipse inspection.

I also tried ensuring resolveClass() is called by hardcoding resolve of the loadClass( name, resolve ) to true, but this makes no difference.

UPDATED AGAIN: Thanks to Holger's excellent "slap-with-a-trout" moment below, I was exceedingly stupid to think that I could infer the meaning of the private variables in the Class objects being returned.

I threw a few System.out.print()s inside my ClassLoader (synchronized of course - the first time I tried it without synchronisation was very messy!) I stuck the following line just before the return statement of loadClass()

System.out.print( clazz.getName() + " annotations:" );
for( final Annotation a : clazz.getAnnotations() )
{
    System.out.print( " " + a.annotationType().getName() + ";" );
}
System.out.println();

This gave me the results I was expecting, printing out something along the lines of:

org.fun.MyClass annotations: org.fun.MyAnnotationOne; org.fun.MyAnnotationsTwo;

But wait a minute - is it working?

System.out.print( clazz.getName() + " annotations:" );
for( final Annotation a : clazz.getAnnotations() )
{
    System.out.print( " " + a.annotationType().getName() + ";" );
}
System.out.print( "HAS_ANNOTATION:" );
if( clazz.getAnnotation( MyAnnotationOne.class ) != null )
{
    System.out.print( "true" );
}
else
{
    System.out.print( "false" );
}
System.out.println();

The result:

org.fun.MyClass annotations: org.fun.MyAnnotationOne; org.fun.MyAnnotationsTwo;HAS_ANNOTATION:false

Yikes! But then it hit me. Check my answer below.

1
“(I inspected the internal fields using Eclipse to discover this.)” This doesn’t mean anything. There is a reason for encapsulation in OOP. Just looking into the private fields of an object doesn’t tell you anything about the semantic. I bet, if you call, e.g. getDeclaredMethods(), on the class instance instead of peeking with a debugger, things will look completely different. - Holger
Excellent comment. The peek came about, as for other classes, these fields appeared to be set and I erroneously assumed that this was linked to the problem. I'll update the question with my new findings. - jr.
For all I could know, those fields could just store previously dug-up values. - jr.

1 Answers

0
votes

I remember reading something interesting about java.lang.Class equality today that completely skipped my mind when I went back to trying to debug this thing. Jon Skeet mentioned in the answer to another question:

Yes, that code is valid - if the two classes have been loaded by the same classloader. If you want the two classes to be treated as equal even if they've been loaded by different classloaders, possibly from different locations, based on the fully-qualified name, then just compare fully-qualified names instead.

Note that your code only considers an exact match, however - it won't provide the sort of "assignment compatibility" that (say) instanceof does when seeing whether a value refers to an object which is an instance of a given class. For that, you'd want to look at Class.isAssignableFrom.

As java.lang.Class does not override java.lang.Object.equals(), and each Class object keeps a reference to its ClassLoader, even if two Classes have the same class, data and name, they won't be equal if they've come from two different ClassLoaders. So how does that apply here?

The problematic line of code is

if( clazz.getAnnotation( MyAnnotationOne.class ) != null )

Calling clazz.getAnnotations(), as above in the final update to the question will list an annotation with a fully-qualified class name that is equal to the fully-qualified class name of the class referenced by the MyAnnotationOne.class literal in the above if statement. However, I'm guessing that clazz.getAnnotation() uses some internal equals() call to check if the annotation classes are the same.

The class referenced by MyAnnotationOne.class is loaded by the custom ClassLoader's classLoader (which in most cases will be its parent). However, the MyAnnotationOne class that is attached to clazz is loaded by the custom ClassLoader itself. As a result, the equals() method returns false, and we get null back.

Thank you very much to Holger for pushing me back in the right direction with this and getting me to the answer in the end.