Friday, January 6, 2012

Iterating over all Classes with an Annotation or Interface

...is impossible unless you use JavaEE, right? Wrong!

With some tricks you can iterate over all classes which fullfill a given predicate, like implementing an interface or wearing an annotation. But why should you care?

Well, software engineering is all about reuse. For example we split our software up in about five modules and build currently four different products on top of that. Since a module obviously has no knowledge about any of there products and their classes, we need to discover extensions and handlers and other hooks at runtime.

Yes, we could use OSGI or Spring for that, but when we started out, but we both of these "feature battleships" are far too large for our concerns. So we built or own little DI (dependency injection) framework (with about a handful of classes). Well DI is probably not the key aspect, it's actually all about getting all classes implemeting a given  interface or annotation. (Some concrete examples will follow in the next posts).

So how do we get this magic list? Well, it's tricky - but work's like a charm in our setting:

As I said, we have several modules which each will participate in the seach for classes. Therefore each module will become a JAR file. Now what we do is, we place a file called "component.properties" in the root folder of this JAR, also in the root folder of the Eclipse project repsectively. This file contains some meta-data like name, version and build-date (filled by ant) - but that's irrelevant now.

Now when we want to discover our classes, we first get a list of all component.properties in the classpath, using the technique above, there will be one per module/JAR:

Enumeration<URL> e = Nucleus.class.getClassLoader().
                       getResources("component.properties");

We then use each of the returned URLs and apply the following (dirty) algorithm:

    /**
     * Takes a given url and creates a list which contains 
     * all children of the given url. 
     * (Works with Files and JARs).
     */
    public static List<String> getChildren(URL url) {
        List<String> result = new ArrayList<String>();
        if ("file".equals(url.getProtocol())) {
            File file = new File(url.getPath());
            if (!file.isDirectory()) {
                file = file.getParentFile();
            }
            addFiles(file, result, file);
        } else if ("jar".equals(url.getProtocol())) {
            try {
                JarFile jar = ((JarURLConnection)
                                url.openConnection())
.getJarFile();
                Enumeration<JarEntry> e = jar.entries();
                while (e.hasMoreElements()) {
                    JarEntry entry = e.nextElement();
                    result.add(entry.getName());
                }
            } catch (IOException e) {
                Log.UTIL.WARN(e);
            }
        }
        return result;
    }

    /**
     * Collects all children of the given file into the given 
     * result list. The resulting string is the relative path
     * from the given reference.
     */
    private static void addFiles(File file, 
                                 List<String> result, 
                                 File reference) 
    {
        if (!file.exists() || !file.isDirectory()) {
            return;
        }
        for (File child : file.listFiles()) {
            if (child.isDirectory()) {
                addFiles(child, result, reference);
            } else {
                String path = null;
                while (child != null && !child.equals(reference)) {
                    if (path != null) {
                        path = child.getName() + "/" + path;
                    } else {
                        path = child.getName();
                    }
                    child = child.getParentFile();
                }
                result.add(path);
            }
        }
    }


So what we now have is a list of files like:
    com/acme/MyClass.class
    com/acme/resource.txt
    ... 

We now can filter this list for classes, and load each one, we then can check our predicates (implements interface, has annotation):

...iterating over each result of getChildren(url)...:


if (relativePath.endsWith(".class")) {
  // Remove .class and change / to .
  String className = relativePath.substring(0,
                      relativePath.length() - 6).replace("/", ".");
  try {
     Class<?> clazz = Class.forName(className);
     for (ClassLoadAction action : actions) {
        action.handle(clazz);
     }
  } catch (ClassNotFoundException e) {
     System.err.println("Failed to load class: " + className);
  } catch (NoClassDefFoundError e) {
     System.err.println("Failed to load dependend class: " +
                        className);
  }
}

Now you have loaded all classes which match your predicates. We do this once on startup and fill a lookup Map. Once a component wants to know all implementations of X, we simply query this Map. Furthermore we use the component.properties to let the framework know which component depends on which. We then load the classes in the correct order. This is important, since while loading classes you can provide more implementations of ClassLoadAction - Yes, you can extend the extension loader while loading extensions.

I've made the framework open source. An article about it can be found here: http://andreas.haufler.info/2012/01/modular-java-applications-microkernel.html

This post is the third part of the my series "Enterprisy Java" - We share our hints and tricks how to overcome the obstacles when trying to build several multi tenant web applications out of a set of common modules.

1 comment:

  1. The Best Slots | Casino Roll
    The best slots at Casino Roll. If you love table https://sol.edu.kg/ games, to https://tricktactoe.com/ play blackjack, you gri-go.com have to bet twice for the dealer to 바카라 사이트 win. The dealer poormansguidetocasinogambling must

    ReplyDelete