Sunday 12 October 2008

Scanning classpath annotated classes with Spring 2.5

One of the common problems of using annotations as class markers, substituting configuration files for frameworks metadata is the processing phase of these annotations.

Even if this is not a big difficulty, problems, and potential bugs related to this task like those related to classpath scanning, and resources manipulation are significant and impose the use of low level frameworks like ASM or Javassist (to avoid class loading related problems) which are not very trivial to use nor well documented.

Fortunately Spring 2.5, provides a very nice framework which resolves ‘all’ these problems in a very clean way, using ASM under the woods. This is, more precisely, located in the core module which contains also the i/o and utilities frameworks and can be used without being in the context of using the whole spring container.

In fact, suppose that we have a custom annotation called Marker that have the attribute type. Now suppose that we want to scan the classpath and find all classes annotated by this annotation and having a particular value for the type attribute; In the following snippet of code I show how to satisfy this need.



package example.annotation.scan;

import java.io.IOException;

import java.util.HashSet;

import java.util.Map;

import java.util.Set;

import java.util.Map.Entry;

import org.springframework.core.io.Resource;

import org.springframework.core.io.support.PathMatchingResourcePatternResolver;

import org.springframework.core.io.support.ResourcePatternResolver;

import org.springframework.core.type.AnnotationMetadata;

import org.springframework.core.type.classreading.MetadataReader;

import org.springframework.core.type.classreading.MetadataReaderFactory;

import org.springframework.core.type.classreading.SimpleMetadataReaderFactory;

import org.springframework.core.type.filter.AnnotationTypeFilter;

import org.springframework.core.type.filter.TypeFilter;

import org.springframework.util.ClassUtils;

/**

* Acts as a classpath scanner that finds classes annotated with

* the {@link Marker} annotation. It garantees that no scanned class

* is loaded by the {@link ClassLoader}.

*

* @author slim tebourbi

*

*/

public class MarkedClassFinder {

private final static String FOLDERS_SEPARATOR_AS_STRING = System

.getProperty("file.separator");

/*

* the base package wich represents the root of the scanned resources/classes folder.

*/

public final static String BASE_SCANNING_PACKAGE = "base\\scanned\\classes\\";

private final Class<Marker> MARKER_ANNOTATION = Marker.class;

private final String TYPE_MARKER_ATTRIBUTE = "type";

private final MetadataReaderFactory metadataReaderFactory;

private final TypeFilter annotationFilter;

private final ResourcePatternResolver resourceResolver;

public MarkedClassFinder() {

this.metadataReaderFactory = new SimpleMetadataReaderFactory();

this.annotationFilter = new AnnotationTypeFilter(MARKER_ANNOTATION);

this.resourceResolver = new PathMatchingResourcePatternResolver(

Thread.currentThread().getContextClassLoader());

}

public Set> findMarkedClassOfType(String type) {

Set> markedClasses = new HashSet>();

/*

* First of all we load all resources that are under a specific package by using a ResourcePatternResolver.

* By doing so we will use class files as simple resources without passing

* by the ClassLoader which can for example cause the execution of a static initialization block.

* It resolves also transparently resources in jars.

*/

String candidateClassesLocationPattern = "classpath*:" + BASE_SCANNING_PACKAGE + "**" + FOLDERS_SEPARATOR_AS_STRING + "*.class";

Resource[] resources = null;

try {

resources = resourceResolver.getResources(candidateClassesLocationPattern);

} catch (IOException e) {

throw new RuntimeException(

"An I/O problem occurs when trying to resolve ressources matching the pattern : "

+ candidateClassesLocationPattern, e);

}

/*

* then we proceed resource by resource, using a MetadataReaderFactory to create

* MetadataReader wich hides the ASM related interface and complexity.

*

*/

for (Resource resource : resources) {

MetadataReader metadataReader = null;

try {

metadataReader = this.metadataReaderFactory.getMetadataReader(resource);

/*

* the filter will pass only annotated classes

*/

if (this.annotationFilter.match(metadataReader, metadataReaderFactory)) {

/*

* the AnnotationMetadata is a simple abstaction of the informations

* that holds the annotation

*/

AnnotationMetadata metadata = metadataReader.getAnnotationMetadata();

Map attrts = metadata.getAnnotationAttributes(MARKER_ANNOTATION.getName());

for (Entry attrt : attrts.entrySet()) {

if ((TYPE_MARKER_ATTRIBUTE.equals(attrt.getKey())) && (type.equals(attrt.getValue())))

{

String className = convertResourceToClassName(resource,BASE_SCANNING_PACKAGE);

try {

markedClasses.add(ClassUtils.forName(className));

} catch (Exception e) {

throw new RuntimeException("problems occurs when trying to load the annotated class : " + className,e);

}

}

}

}

} catch (IOException e) {

throw new RuntimeException("An I/O problem occurs when trying to process resource : " + resource,e);

}

}

return markedClasses;

}

static String convertResourceToClassName(Resource resource,String basePackage) throws IOException {

String path = resource.getFile().getPath();

String pathWithoutSuffix = path.substring(0, path.length()- ClassUtils.CLASS_FILE_SUFFIX.length());

String relativePathWithoutSuffix = pathWithoutSuffix.substring(pathWithoutSuffix.indexOf(basePackage));

String className = relativePathWithoutSuffix.replace('\\', '.');

return className;

}

}


6 comments:

  1. Very nice!
    Its a bit hard to read the code (no indentation).

    ReplyDelete
  2. And what do I need to do to retrieve method-level annotations (instead of class-level) ?

    This has been bugging me all afternoon .... :)

    ReplyDelete
  3. Nice post, but want to clarify one thing, there is simpler way to guess className:

    String className = metadataReader.getClassMetadata().getClassName();

    ReplyDelete
  4. Good post for describing how to do it with Spring. Unfortunately it's a lot of boiler plate code.

    If you want to remove the clutter and get rid of the Spring dependency for just doing some class path scanning consider using eXtcos, the Extensible Component Scanner, available at http://sourceforge.net/projects/extcos.

    With eXtcos searching for all classes in some "sample" package annotated with @MyAnnotation gets as simple as:

    ClasspathScanner scanner = new ClasspathScanner();

    Set classes = scanner.getClasses(new ClassQuery() {
    protected void query() {
    select().
    from(“sample”).
    returning(allAnnotatedWith(MyAnnotation.class));
    }
    });

    Like it? Test it! - And enjoy ;-)

    http://sourceforge.net/projects/extcos

    ReplyDelete
  5. But eXtcos, it internally uses JBoss VFS and Google Guice. No magic there as well. And moreover, the idea has very little sanity; do not depend on one DI framework (Spring), but happily use the other (Guice).

    Also, eXtcos is very complicated in nature and for me it does not work, mainly for 2 reasons:
    - My entire project is in Spring; so I would like to use Spting for my ALL DI needs; why do I use another similar framework (Google Guice) if a DI is required?
    - who will provide support to this JAR if something breaks.

    ReplyDelete
  6. Thanks for investigating eXtcos and your kind critique.

    I'd like to point out a few things, though. First of all eXtcos doesn't depend on any DI framework anymore as of version 0.3b. This version has been released on 07 November 2011 already. So the version you investigated is out-of-date anyway. But your point is right anyway.

    Secondly as you point out correctly eXtcos supports JBoss VFS. However it only does so at runtime when used inside JBoss and hasn't got any compile time dependency on it. This is achieved by reflection, and adds a nice feature in a light-weight way.

    Support is provided by me. Just write me an email at mimarox@users.sourceforge.net. If needed I could even provide professional consulting at a fair rate.

    You write that eXtcos is "very complicated in nature". Please elaborate on this, so I can make it easier to use. Thanks.

    ReplyDelete