Thursday, December 20, 2007

Scanning the Class Path with Spring 2.5

I was writing a test to check the consistency of a collection of classes. The desired behavior was to scan the class path starting from a base package, to filter the classes using some criteria and to check the resulting classes for the presence of a combination of annotations. I needed a piece of code that takes care of the scanning bit. It’s not rocket science but this kind of code could be recurrently useful.
I happened to remember that the newly released Spring 2.5 integrates a similar behavior. I peeked into the code and found that ClassPathScanningCandidateComponentProvider was responsible for that part. The key method is findCandidateComponents, which returns a set of bean definitions.

Here's a method based on ClassPathScanningCandidateComponentProvider that loads and returns the classes it finds:

public Set<Class<?>> scanClassPath(String basePackage,
 TypeFilter includeFilter, TypeFilter excludeFilter)
 throws Exception {
 Set<Class<?>> result = new HashSet<Class<?>>();
 ResourcePatternResolver resourcePatternResolver = 
  new PathMatchingResourcePatternResolver();
 String packageSearchPath = 
  ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX;
 if (basePackage != null) {
  packageSearchPath += ClassUtils
   .convertClassNameToResourcePath(basePackage);
 }
 packageSearchPath += "/**/*.class";

 MetadataReaderFactory metadataReaderFactory = 
  new CachingMetadataReaderFactory(
   resourcePatternResolver);

 Resource[] resources = resourcePatternResolver
   .getResources(packageSearchPath);

 for (Resource resource : resources) {
  MetadataReader metadataReader = metadataReaderFactory
    .getMetadataReader(resource);
  ClassMetadata classMetadata = 
   metadataReader.getClassMetadata();

  if (includeFilter != null
    && !includeFilter.match(metadataReader,
      metadataReaderFactory)) {
   continue;
  }

  if (excludeFilter != null
    && excludeFilter.match(metadataReader,
      metadataReaderFactory)) {
   continue;
  }
  Class<?> class1 = Class.
   forName(classMetadata.getClassName());
  result.add(class1);
 }
 return result;
}
Advantages:
  • Class files are treated as plain files, no loading occurs before a class is needed.
  • MetadataReaders provide useful information about the scanned classes without loading them, including the name of the class, whether it is concrete or abstract, the name of its super class and the implemented interfaces and even information about its annotations.
  • TypeFilters can be used to include or exclude classes. Spring 2.5 provides a couple of implementations.
It is also possible to subclass ClassPathScanningCandidateComponentProvider in order to reuse its behavior. Here’s an example with an anonymous class that loads the classes returned by findCandidateComponents:

result = new ClassPathScanningCandidateComponentProvider(false){
 {
  addIncludeFilter(
   new AssignableTypeFilter(A.class));
  addExcludeFilter(
   new AssignableTypeFilter(B.class));
 }
 Set<Class<?>> scan(String basePackage) {
  Set<Class<?>> classes = new HashSet<Class<?>>();
  Set<BeanDefinition> beanDefinitions = 
   findCandidateComponents(basePackage);
  for (BeanDefinition def : beanDefinitions) {
   try {
    classes.add(
     Class.forName(def.getBeanClassName()));
   } catch (ClassNotFoundException e) {}
  }
  return classes;
 }
}.scan("my.base.package");