It seems I'm doing a very similar thing. While in the end component frameworks such as OSGi and the NetBeans Platform (that can be uses also server-side) are a workable solution that I've used and I'm using for other projects, they pay their complexity when you use more features that they offer, beyond searching for registered components (e.g. enforcing dependencies checks, version checks, module isolation, etc...).
But for scanning bundled classes there's a simpler solution, based on annotation scanning. In my project with Vaadin I'm creating a user interface referring to "abstract" component names, that have to be matched by actual Java classes that might be provided by a user.
Java classes implementing a component are marked with a custom-made annotation: e.g.
@ViewMetadata(typeUri="component/HtmlTextWithTitle", controlledBy=DefaultHtmlTextWithTitleViewController.class)
public class VaadinHtmlTextWithTitleView extends Label implements HtmlTextWithTitleView
Then I search for annotated classes in the classpath with a ClassScanner:
final ClassScanner classScanner = new ClassScanner();
classScanner.addIncludeFilter(new AnnotationTypeFilter(ViewMetadata.class));
for (final Class<?> viewClass : classScanner.findClasses())
{
final ViewMetadata viewMetadata = viewClass.getAnnotation(ViewMetadata.class);
final String typeUri = viewMetadata.typeUri();
// etc...
}
This is my complete implementation for the ClassScanner, implemented on top of Spring:
import javax.annotation.Nonnull;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.core.type.filter.TypeFilter;
import org.springframework.util.ClassUtils;
public class ClassScanner
{
private final String basePackage = "it"; // FIXME
private final ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false);
@Nonnull
public final Collection<Class<?>> findClasses()
{
final List<Class<?>> classes = new ArrayList<Class<?>>();
for (final BeanDefinition candidate : scanner.findCandidateComponents(basePackage))
{
classes.add(ClassUtils.resolveClassName(candidate.getBeanClassName(), ClassUtils.getDefaultClassLoader()));
}
return classes;
}
public void addIncludeFilter (final @Nonnull TypeFilter filter)
{
scanner.addIncludeFilter(filter);
}
}
It's very simple, but effective. Note that, because of how Java ClassLoaders work, you have to specify at least one package to search into. In my example I hardwired the top package "it" (my stuff is "it.tidalwave.*"), it's easy to put this information in a property that can be configured, eventually specifying more than one package.
Another solution could be used by just using two libraries from the NetBeans Platform. I stress the concept that this wouldn't import the whole platform into your project, including the classloader facilities etc., but just using two jar files. Thus it's not invasive. The libraries are org-openide-util.jar and org-openide-util-lookup.jar (I stress again, you can use the plain .jar files instead of the .nbm files that are specific of the NetBeans Platform).
Basically, you'd use the @ServiceProvider annotation. It gets triggered during compilation (with Java 6) and generates a META-INF/services/ description file that will be placed in the classpath. This file is a standard feature of Java (since 1.3, I believe) and can be queried with the standard class ServiceLoader. In this case, you'd use the NetBeans Platform libraries only during compilation, because they are only used for generating META-INF/services. Eventually, the libraries could be used also for better ways to query the registered services, by means of the Lookup class.
There's a design difference between the two solutions. With my custom annotation, I'm discovering classes: then I use them with reflection to instantiate objects. With @ServiceProvider the system automatically instantiates a 'singleton' object from the class. Thus in the former case I register the classes for the objects I want to create, in the second cases I register a factory for creating them. In this case, it seems that the former solution requires one less passage, and that's why I'm using it (normally, I use @ServiceProvider a lot).
Summing up, three solutions have been enumerated:
- Use my provided ClassScanner with Spring. Requires Spring in the runtime.
- Use @ServiceProvider in code and scan with ServiceLoader. Requires two NetBeans Platform libraries at compile-time, and just the Java Runtime at runtime.
- Use @ServiceProvider in code and scan with Lookup. Requires two NetBeans Platform libraries at runtime.
You might also look at answers to this question.