Note that there are some explanatory texts on larger screens.

plurals
  1. POImplementing custom ClassLoader to scan /WEB-INF/classes directory
    primarykey
    data
    text
    <p>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.</p> <p>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.</p> <p>Unfortunately, I'm getting a nasty <code>java.lang.NoClassDefFoundError: javax/websocket/server/ServerEndpointConfig$Configurator</code> exception when it's attempting to load one of my ServerEndpointConfigurators.</p> <pre><code>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&lt;?&gt; loadClass( final String name, final boolean resolve ) throws ClassNotFoundException { synchronized( getClassLoadingLock( name ) ) { Class&lt;?&gt; 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(); } } </code></pre> <p>Some of the code was influenced by the default ClassLoader implementation.</p> <p>Here's the general flow of what I'm attempting to do:</p> <ol> <li>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.</li> <li>Check to see if i'm already loaded this class using findLoadedclass();</li> <li>If it's not already loaded, attempt to pull in the class from my "WEB-INF/classes" directory.</li> <li>If this fails, delegate to the parent classloader.</li> <li>If this still fails, blow up (throw).</li> </ol> <p>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.</p> <p>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.</p> <p>Let me know if you also happen to notice anything particularly insidious / distasteful that I'm doing other than causing it not to work.</p> <p><strong>UPDATE</strong>: 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.</p> <p>I added the following line as the first line in my constructor:</p> <pre><code>super( Thread.currentThread().getContextClassLoader() ); </code></pre> <p>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.)</p> <p><strong>MORE INFO</strong>: 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 (<em>I've hidden the package names to prevent sharing unnecessary project info</em>). <img src="https://i.imgur.com/4q7AGG0.png" alt="Eclipse inspection">.</p> <p>I also tried ensuring resolveClass() is called by hardcoding resolve of the loadClass( name, resolve ) to true, but this makes no difference.</p> <p><strong>UPDATED AGAIN</strong>: 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 <code>Class</code> objects being returned.</p> <p>I threw a few <code>System.out.print()</code>s inside my <code>ClassLoader</code> (<code>synchronized</code> of course - the first time I tried it without synchronisation was very messy!) I stuck the following line just before the return statement of <code>loadClass()</code></p> <pre><code>System.out.print( clazz.getName() + " annotations:" ); for( final Annotation a : clazz.getAnnotations() ) { System.out.print( " " + a.annotationType().getName() + ";" ); } System.out.println(); </code></pre> <p>This gave me the results I was expecting, printing out something along the lines of:</p> <pre><code>org.fun.MyClass annotations: org.fun.MyAnnotationOne; org.fun.MyAnnotationsTwo; </code></pre> <p>But wait a minute - is it working?</p> <pre><code>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(); </code></pre> <p>The result: </p> <pre><code>org.fun.MyClass annotations: org.fun.MyAnnotationOne; org.fun.MyAnnotationsTwo;HAS_ANNOTATION:false </code></pre> <p>Yikes! But then it hit me. Check my answer below.</p>
    singulars
    1. This table or related slice is empty.
    plurals
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    1. This table or related slice is empty.
 

Querying!

 
Guidance

SQuiL has stopped working due to an internal error.

If you are curious you may find further information in the browser console, which is accessible through the devtools (F12).

Reload