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;

}

}