find all implementors of an interface?

Definitely agree here :slight_smile:

Good API design is very hard, yes. Since it’s my specialism, I’m more demanding than most ::). There are many bits of java that are excellent, but I get frustrated when the quality of API’s (and/or documentation - e.g Swing has both some really really good docs, e.g. a 4 page class-comment IIRC, and some really terrible ones, e.g. some classes with no comments at all!) has very large variance.

Not exactly a timely post, but here is code to find all subclasses of a class (or all implementors of an interface) on your classpath.

To use it, add these classes to your classpath and from somewhere in your program call:
Class[] interestingClasses = Subclasses.of( MyInterface.class );
// or add a regex
Class[] interestingClasses = Subclasses.of( MyClass.class, ".*Suffix$ );
Once you have the array of classes you can instantiate them with relfection, but I beleive that only works if your class has an empty constructor.


import java.util.ArrayList;
import java.util.Iterator;

/**
 * Created with Eclipse 
 * User: duncanIdaho for java-gaming.org
 * 
 * copyright 2004
 */

/**
 * A class to find all subclasses of a given <code>Class</code> or interface.
 * 
 */
public class Subclasses {
      
      /**
       * Find all subclasses of the given <code>Class</code> or interface by
       * loading all classes on the class path.
       * 
       * @param targetType the superclass of all returned classes.
       * @return an array of all subclasses of <code>targetType</code>
       */
      public static Class[] of( Class targetType ) {
            return of( targetType, ".*" );
            
      }
      
      /**
       * Find all subclasses of the given <code>Class</code> or interface by
       * loading only those classes with names that match the given regular
       * expression.
       * 
       * @param targetType the superclass of all returned classes.
       * @param regex a regular expression that will match with every subclass
       * @return an array of all subclasses of <code>targetType</code>
       */
      public static Class[] of( Class targetType, String regex ) {
            ArrayList matches = new ArrayList();
            ClassPath cp = new ClassPath();
            Iterator i = cp.classNameIterator();
            while ( i.hasNext() ) {
                  String className = (String)i.next();
                  if ( className.matches( regex ) &&
                              !className.equals( targetType.getName() ) ) {
                        Class clazz = null;
                        try {
                              clazz = Class.forName( className );
                              
                        } catch (ClassNotFoundException cnfx ) {
                              continue;
                              
                        } finally {
                              if ( clazz != null && targetType.isAssignableFrom( clazz ) ) {
                                    matches.add( clazz );
                                    
                              } 
                              
                        }
                        
                  }
                  
            }
            return (Class[])matches.toArray( new Class[0] );
            
      }
      
}


import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.StringTokenizer;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;


/**
 * Created with Eclipse 
 * User: duncanIdaho for java-gaming.org
 * 
 * copyright 2004
 */


/**
 * ClassPath find and records the fully qualified name of every Class 
 * on the classpath via the system property "java.class.path".
 * 
 */
public class ClassPath {
      
      public static final int SILENT = 0;
      public static final int QUIET = 1;
      public static final int VERBOSE = 2;
      
      public static final String JAR_EXT = ".jar";
      public static final String ZIP_EXT = ".zip";
      public static final String CLASS_EXT = ".class";

      private ArrayList classNames;
      
      private int outputLevel;
      
      /**
       * create a new ClassPath instance and find all classes on the classpath
       *
       */
      public ClassPath() {
            this( SILENT );
            
      }
      
      
      public ClassPath(int type) {
            super();
            outputLevel = type;
            findAllClassNames();
      
      }
      
      /**
       * Answers an <code>Iterator</code> over fully qualified <code>Class</code>
       * names found on the classpath.
       * @return an <code>Iterator</code> over the elements in this list
       */
      public Iterator classNameIterator() {
            return classNames.iterator();
            
      }
      
      /**
       * Answers an <code>ArrayList</code> of all <code>Class</code> names found
       * on the classpath.
       * @return an <code>ArrayList</code> of class names.
       */
      public ArrayList getClassNames() {
            return classNames;
            
      }

      /**
       * Initialize the member variable <code>classNames</code> and 
       * look for classes.
       *
       */
      private void findAllClassNames() {
            String path = null;
            classNames = new ArrayList();
            try {
                  path = System.getProperty( "java.class.path" );
            } catch ( Exception x ) {
                  x.printStackTrace();
                  
            }
            if ( outputLevel != SILENT )
                  System.out.println( "scanning classpath: " + path );
            StringTokenizer toke = new StringTokenizer( path, File.pathSeparator );
            while ( toke.hasMoreTokens()) {
                  String pathElement = toke.nextToken();
                  File elementFile = new File( pathElement );
                  String elementName = elementFile.getAbsolutePath();
                  if ( elementName.endsWith( JAR_EXT ) ) {
                        addJarContents( elementFile );
                        
                  } else if ( elementName.endsWith( ZIP_EXT ) ) {
                        addZipContents( elementFile );
                        
                  } else if ( elementName.endsWith( CLASS_EXT ) ) {
                        addClass( elementFile );
                        
                  } else {
                        addDirectoryContents( elementFile );
                        
                  }

            }
            if ( outputLevel != SILENT )
                  System.out.println( "found " + classNames.size() + " classes." );
            
            if ( outputLevel == VERBOSE ) {
                  Iterator i = classNames.iterator();
                  while ( i.hasNext() ) {
                        String name = (String)i.next();
                        System.out.println( name );
                        
                  }
                  
            }
            
      }
      
      /**
       * Adds a file explicitly mentioned on the classpath to the list 
       * of classes.
       * @param classFile a class file listed on the classpath itself.
       */
      private void addClass( File classFile ) {
            classNames.add( getClassNameFrom( classFile.getName() ) );
            
      }
      
      /**
       * Adds all class names found in the jar.
       * @param jarFile a jar file explicitly listed on the classpath.
       */
      private void addJarContents( File jarFile ) {
            JarFile jar = null;
            try {
                  jar = new JarFile( jarFile );

            } catch ( IOException iox ) {
                  // boom!
            }
            if ( jar != null ) {
                  Enumeration e = jar.entries();
                  while (e.hasMoreElements()) {
                        JarEntry entry = (JarEntry)e.nextElement();
                        if ( !entry.isDirectory() && entry.getName().endsWith( CLASS_EXT ) ) {
                              String className = getClassNameFrom( entry.getName() );
                              classNames.add( className );

                        }
                  }      
            }
      }
      
      /**
       * Adds all class names found in the zip mentioned 
       * @param zipFile
       */
      private void addZipContents( File zipFile ) {
            ZipFile zip = null;
            try {
                  zip = new JarFile( zipFile );
                  
            } catch ( IOException iox ) {
                  
            }
            if ( zip != null ) {
                  Enumeration e = zip.entries();
                  while (e.hasMoreElements()) {
                        ZipEntry entry = (ZipEntry)e.nextElement();
                        if ( !entry.isDirectory() && entry.getName().endsWith( CLASS_EXT ) ) {
                              String className = getClassNameFrom( entry.getName() );
                              classNames.add( className );
                              
                        }
                  }      
            }      
      }

      /**
       * This method takes a top level classpath dir i.e. 'classes' or bin
       * @param dir
       */
      private void addDirectoryContents( File dir ) {
            // drill through contained dirs ... this is expected to be the 
            // 'classes' or 'bin' dir
            File files[] = dir.listFiles();
            for( int i = 0; i < files.length ; ++i ) {
                  File f = files[i];
                  if ( f.isDirectory() ) {
                        addDirectoryContents( "", f );
                        
                  } else {
                        if ( f.getName().endsWith( CLASS_EXT ) )
                              addClass( f );
                                    
                  }
                  
            }
            
      }
      
      /**
       * This method does the real directory recursion, passing along the 
       * the corresponding package-path to this directory.
       * 
       * @param pathTo the preceding path to this directory
       * @param dir a directory to search for class files
       */
      private void addDirectoryContents( String pathTo, File dir ) {
            String pathToHere = pathTo + dir.getName() + File.separator;
            File files[] = dir.listFiles();
            for( int i = 0; i < files.length ; ++i ) {
                  File f = files[i];
                  if ( f.isDirectory() ) {
                        addDirectoryContents( pathToHere, f );
                        
                  } else {
                        if ( f.getName().endsWith( CLASS_EXT ) ) {
                              String absFilePath = pathToHere + f.getName();
                              classNames.add( getClassNameFrom( absFilePath ) );
                              
                        }
                                    
                  }
                  
            }
            
      }
      
      /**
       * replace ANY slashes with dots and remove the .class at the 
       * end of the file name.
       * @param entryName a file name relative to the classpath.  A class 
       * of package org found in directory bin would be passed into this 
       * method as "org/MyClass.class"
       * @return a fully qualified Class name.
       */
      private String getClassNameFrom( String entryName ) {
            String foo = new String(entryName).replace( '/', '.' );
            foo = foo.replace( '\\', '.' );
            return foo.substring( 0, foo.lastIndexOf( '.' ) );
            
      }

}

I’ve written a plugin loader for a PVR software application I’m writing. Here is the code, it’ll scan through all jars in a given directory and find me classes inside those jars which implement my plugin interface.


package com.swizel.javamediacenter.plugins;

import java.io.File;
import java.io.IOException;
import java.util.Vector;
import java.util.Enumeration;
import java.util.zip.ZipEntry;
import java.util.jar.JarFile;
import java.net.URLClassLoader;
import java.net.URL;
import java.net.MalformedURLException;

import com.swizel.javamediacenter.io.*;

public class PluginLoader {

  private static final PluginLoader thisInstance = new PluginLoader();
  private static URLClassLoader pluginClassLoader;
  private static Vector pluginJarFiles;
  private static Object[] pluginClasses;

  private PluginLoader() {
    // Scan the plugins directory for jar files.
    File pluginsDir  = new File("plugins");
    File[] jarFiles  = pluginsDir.listFiles(new JarFileFilter());

    // Loop through jar files and add then to a plugin class loader
    URL[]  lvJarURLs = new URL[jarFiles.length];
    pluginJarFiles = new Vector();
    for (int i=0; i<jarFiles.length; i++) {
      try {
        lvJarURLs[i] = jarFiles[i].toURL();
        pluginJarFiles.addElement(new JarFile(jarFiles[i]));
      } catch (MalformedURLException murle) {
      } catch (IOException ioe) {
      }
    }
    pluginClassLoader = new URLClassLoader(lvJarURLs);
  }

  public static PluginLoader getInstance() {
    return thisInstance;
  }

  public static URLClassLoader getClassLoader() {
    return pluginClassLoader;
  }

  public static Object[] getPlugins() {
    if (pluginClasses == null) {
      Vector lvPluginClasses = new Vector();

      // Load each jar file and scan through classes looking for plugins
      for (int i=0; i<pluginJarFiles.size(); i++) {
        JarFile lvJar = (JarFile) pluginJarFiles.elementAt(i);

        Class lvPlugin = findPluginInJarFile(lvJar);
        if (lvPlugin != null) {
          try {
            lvPluginClasses.addElement(lvPlugin.newInstance());
          } catch (Exception e) {
            System.out.println("Can't create plugin : " + lvPlugin.getName());
          }
        }
      }

      pluginClasses = lvPluginClasses.toArray();
    }

    return pluginClasses;
  }

  private static Class findPluginInJarFile(JarFile pJarFile) {
    // This method assumes that there is only 1 plugin per jar file.
    Enumeration lvEntries = pJarFile.entries();

    while (lvEntries.hasMoreElements()) {
      ZipEntry lvFile = (ZipEntry) lvEntries.nextElement();

      // Should I exclude classes here that contain a $ in their name since they're inner classes?
      if ((!lvFile.isDirectory()) && (lvFile.getName().trim().toLowerCase().endsWith(".class"))) {
        String lvName = lvFile.getName().replace('/', '.').substring(0, lvFile.getName().length() - 6);

        try {
          Class lvClass = pluginClassLoader.loadClass(lvName);
          Class[] lvInterfaces = lvClass.getInterfaces();
          for (int j=0; j<lvInterfaces.length; j++) {
            // Remember each class that implements one of our interfaces.
            if (PluginInterface.class.isAssignableFrom(lvInterfaces[j])) {
              return lvClass;
            }
          }
        } catch (Exception e) {
          System.out.println(lvName + " : " + e);
        }
      }
    }

    return null;
  }


}

This code is limited to finding only 1 class per jar file that implements my interface, which is ok for my app that I’m writing, you may want to change this.

Andy.

The file scanning code above is flawed in that it won’t work correctly where jar files have a class-path entry in the manifest. This invisibly adds additional jar files to the class path.

There are two main uses for this type of functionality
a) in an IDE (for refactoring or other developer search aids). In this case you will already know where all the relevant source and class files are located and can search them pretty much as described in the other posts.

b) You want to find all available implementations for some service. In this case you (the service implementors) explicitly list the implementing classes in a text resource. The text resources are located using ClassLoader.getResources() as this will allow each jar file or directory tree on the class path to have an instance of the resource. Merge all the results you thus obtain. I think there is even a semi-standard implementation of this.

Good point.

This version of ClassPath fixes that issue ( use with SubClasses above ) by scanning the manifest and digging through any Class-Path entries.

note: javac assumes Class-Path entries are relative to the path of the jar they are in, so I do as well!! I have tested this with a main.jar, a second.jar referenced in main’s manifest, and a third.jar referenced in second.jar’s manifest.


import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;


/**
 * Created with Eclipse 
 * User: duncanIdaho for java-gaming.org
 *
 */

/**
 * ClassPath find and records the fully qualified name of every Class 
 * on the classpath via the system property "java.class.path".
 * 
 */
public class ClassPath {
      
      public static final int SILENT = 0;
      public static final int QUIET = 1;
      public static final int VERBOSE = 2;
      
      public static final String JAR_EXT = ".jar";
      public static final String ZIP_EXT = ".zip";
      public static final String CLASS_EXT = ".class";

      private ArrayList classNames;
      
      private int outputLevel;
      
      /**
       * create a new ClassPath instance and find all classes on the classpath
       *
       */
      public ClassPath() {
            this( SILENT );
            
      }
      
      
      public ClassPath(int type) {
            super();
            outputLevel = type;
            findAllClassNames();
      
      }
      
      /**
       * Answers an <code>Iterator</code> over fully qualified <code>Class</code>
       * names found on the classpath.
       * @return an <code>Iterator</code> over the elements in this list
       */
      public Iterator classNameIterator() {
            return classNames.iterator();
            
      }
      
      /**
       * Answers an <code>ArrayList</code> of all <code>Class</code> names found
       * on the classpath.
       * @return an <code>ArrayList</code> of class names.
       */
      public ArrayList getClassNames() {
            return classNames;
            
      }

      /**
       * Initialize the member variable <code>classNames</code> and 
       * look for classes.
       *
       */
      private void findAllClassNames() {
            String path = null;
            classNames = new ArrayList();
            try {
                  path = System.getProperty( "java.class.path" );
            } catch ( Exception x ) {
                  x.printStackTrace();
                  
            }
            if ( outputLevel != SILENT )
                  System.out.println( "scanning classpath: " + path );
            StringTokenizer toke = new StringTokenizer( path, File.pathSeparator );
            
            analyzeClasspathTokens( toke );

            if ( outputLevel != SILENT )
                  System.out.println( "found " + classNames.size() + " classes." );
            
            if ( outputLevel == VERBOSE ) {
                  Iterator i = classNames.iterator();
                  while ( i.hasNext() ) {
                        String name = (String)i.next();
                        System.out.println( name );
                        
                  }
                  
            }
            
      }
      
      /**
       * Adds a file explicitly mentioned on the classpath to the list 
       * of classes.
       * @param classFile a class file listed on the classpath itself.
       */
      private void addClass( File classFile ) {
            classNames.add( getClassNameFrom( classFile.getName() ) );
            
      }
      
      /**
       * Adds all class names found in the jar.
       * @param jarFile a jar file explicitly listed on the classpath.
       */
      private void addJarContents( File jarFile ) {
            JarFile jar = null;
            try {
                  jar = new JarFile( jarFile );

            } catch ( IOException iox ) {
                  // boom!
            }
            if ( jar != null ) {
                  Manifest man = null;
                  try { 
                        man = jar.getManifest();
                  } catch( IOException iox ) {
                        System.err.println("error obtaining manifest from: " + jar.getName());
                        
                  } finally {
                        if ( man != null ) {
                              this.scanClasspath( man, jar, jarFile );
                              
                        }
                        
                  }
                  Enumeration e = jar.entries();
                  while (e.hasMoreElements()) {
                        JarEntry entry = (JarEntry)e.nextElement();
                        if ( !entry.isDirectory() && entry.getName().endsWith( CLASS_EXT ) ) {
                              String className = getClassNameFrom( entry.getName() );
                              classNames.add( className );

                        }
                  
                  }      

            }
            
      }
      
      /**
       * Adds all class names found in the zip mentioned 
       * @param zipFile
       */
      private void addZipContents( File zipFile ) {
            ZipFile zip = null;
            try {
                  zip = new JarFile( zipFile );
                  
            } catch ( IOException iox ) {
                  
            }
            if ( zip != null ) {
                  Enumeration e = zip.entries();
                  while (e.hasMoreElements()) {
                        ZipEntry entry = (ZipEntry)e.nextElement();
                        if ( !entry.isDirectory() && entry.getName().endsWith( CLASS_EXT ) ) {
                              String className = getClassNameFrom( entry.getName() );
                              classNames.add( className );
                              
                        }
                  }      
            }      
      }

      /**
       * This method takes a top level classpath dir i.e. 'classes' or bin
       * @param dir
       */
      private void addDirectoryContents( File dir ) {
            // drill through contained dirs ... this is expected to be the 
            // 'classes' or 'bin' dir
            File files[] = dir.listFiles();
            for( int i = 0; i < files.length ; ++i ) {
                  File f = files[i];
                  if ( f.isDirectory() ) {
                        addDirectoryContents( "", f );
                        
                  } else {
                        if ( f.getName().endsWith( CLASS_EXT ) )
                              addClass( f );
                                    
                  }
                  
            }
            
      }
      
      /**
       * This method does the real directory recursion, passing along the 
       * the corresponding package-path to this directory.
       * 
       * @param pathTo the preceding path to this directory
       * @param dir a directory to search for class files
       */
      private void addDirectoryContents( String pathTo, File dir ) {
            String pathToHere = pathTo + dir.getName() + File.separator;
            File files[] = dir.listFiles();
            for( int i = 0; i < files.length ; ++i ) {
                  File f = files[i];
                  if ( f.isDirectory() ) {
                        addDirectoryContents( pathToHere, f );
                        
                  } else {
                        if ( f.getName().endsWith( CLASS_EXT ) ) {
                              String absFilePath = pathToHere + f.getName();
                              classNames.add( getClassNameFrom( absFilePath ) );
                              
                        }
                                    
                  }
                  
            }
            
      }
      
      /**
       * While the StringTokenizer has classpath elements, attempt to 
       * add any contained classes to the list.
       * 
       * @param toke class path elements
       */
      private void analyzeClasspathTokens( StringTokenizer toke ) {
            while ( toke.hasMoreTokens()) {
                  String pathElement = toke.nextToken();
                  analyzeClasspathElement( pathElement );

            }
            
      }
      
      /**
       * Make a file out of the String, determine which kind of 
       * interesting classpath file it might be, and add it to the list.
       * 
       * @param pathElement 
       */
      private void analyzeClasspathElement( String pathElement ) {
            File elementFile = new File( pathElement );
            String elementName = elementFile.getAbsolutePath();
            if ( elementName.endsWith( JAR_EXT ) ) {
                  addJarContents( elementFile );
                  
            } else if ( elementName.endsWith( ZIP_EXT ) ) {
                  addZipContents( elementFile );
                  
            } else if ( elementName.endsWith( CLASS_EXT ) ) {
                  addClass( elementFile );
                  
            } else {
                  addDirectoryContents( elementFile );
                  
            }
            
      }
      
      /**
       * replace ANY slashes with dots and remove the .class at the 
       * end of the file name.
       * @param entryName a file name relative to the classpath.  A class 
       * of package org found in directory bin would be passed into this 
       * method as "org/MyClass.class"
       * @return a fully qualified Class name.
       */
      private String getClassNameFrom( String entryName ) {
            String foo = new String(entryName).replace( '/', '.' );
            foo = foo.replace( '\\', '.' );
            return foo.substring( 0, foo.lastIndexOf( '.' ) );
            
      }
      
      /**
       * Use the manifest associated with the jar to determine if there are
       * any Class-Path elements names in the jar that should also be scanned.
       * 
       * @param man the manifest of the given jar
       * @param jar the jar associated with the given manifest.
       */
      private void scanClasspath( Manifest man, JarFile jar, File jarFile ) {
            Map map = man.getEntries();
            if ( map != null ) {
                  Attributes atts = man.getMainAttributes();
                  if ( atts != null ) {
                        Set keys = atts.keySet();
                        Iterator i = keys.iterator();
                        while ( i.hasNext() ) {
                              Object key = (Object)i.next();
                              String value = (String)atts.get( key );
                              if ( outputLevel == VERBOSE )
                                    System.out.println( jar.getName() + "  " + key + ": " + value );
                              if ( key.toString().equals( "Class-Path" )) {
                                    if ( outputLevel != SILENT )
                                          System.out.println( "scanning " + jar.getName() +"'s manifest classpath: " + value);
                                    StringTokenizer toke = new StringTokenizer( value );
                                    while (toke.hasMoreTokens() ) {
                                          String element = toke.nextToken();
                                          if ( jarFile.getParent() == null )
                                                analyzeClasspathElement( element );
                                          else {
                                                analyzeClasspathElement( jarFile.getParent() + File.separator +  element );
                                          }
                                          
                                    }
                                    
                              }
                              
                        }
                        
                  }      
                  
            }
            
      }

}

All of a sudden, I need similar functionality myself :).

Jeff’s JUtils plugin system won’t work for me because it only searches in one place.
(also ??? I’m worried it might not work with webstart (I saw several uses of File-based access that weren’t shielded by backup plans using non-File mechanisms…)?)

Duncan’s ClassPath class is what I just spent 15 minutes searching Sun’s JDK for - and I am disappointed that:
a. it doesn’t exist (why the heck not?!??!)
b. …and the whole of the Package class in java.lang.reflect. That thing is an abomination! Not only is it hardcoded to assume you only ever have ONE manifest for a single package (not only is it possible to have arbitrarily many, but in fact is common) but it doesn’t have the most obvious of all methods for a Package class - getClassNames() :(. (In fact, it seems it’s just part of the last-minute hacking Sun did to make J2EE a little less painful - Package is not really “Package”, it’s really a class called “ManifestFilePackageEntry” that someone was too lazy to type the full name for :P)

But rather than matching subclasses, I’m after instances. So, I was wondering if perhaps I could take Duncan’s ClassPath and make an alternative to his SubClasses class which was a “front-end” to a range of searches…so it would include his SubClasses lookup, but also an instances lookup, etc.

And then make it all available as a tiny self-contained JAR, with no reserved rights other than authorship details…

Duncan?

[quote]And then make it all available as a tiny self-contained JAR, with no reserved rights other than authorship details…

Duncan?
[/quote]
I was given permission to post the code with the intent to give back to the community, so I think that sounds great.

I believe if you use the URLClassLoader with a JAR URL, then manifest attributes such as Class-Path are automatically respected. At least that is what seems to happen with my plugin loader.