Adding Plug-Ins To Your Application With Spring

Spring’s classpath scanning support can be leveraged to easily implement an application that supports plugins to be added.

One method would be to simply scan a specific package (for example org.example.myapp.plugin for beans annotated with @Component, and add them to your application context. This has some disadvantes though:

  • If plugins use annotation-based dependency injection internally, that might clash with your context as all plugin beans will end up in your application’s context
  • It is a very unspecific way to load plugin classes. You’ll need to document very thoroughly what kind of plugin interface your application provides.

It’s probably a better idea to provide a specific interface for your extension points, and then allow plugins to implement them. In this article, I’ll describe a way to achieve this with Annotations and Spring. Suppose your extension point interface looks like this:

public interface ProviderPlugin {
    Resource provide();
}

The class PluginLoader introduced below will then allow you to implement your plugin like this:

@MyAppPlugin
public class MyProviderPlugin implements ProviderPlugin {
    public Resource provide() { ... }
}

As you can see, all plugins are annotated with a specific annotation, and implement one or more of the extension point interfaces. A plugin annotation is meta-annotated with Spring’s @Component, which allows us to discover and instantiate the plugin using classpath-scanning and a Spring application context.

@Component
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
public @interface MyAppPlugin {
}

The PluginLoader class contains the plugin lookup and instantiation logic. It is parametrized with the plugin annotation type and the base package to search for plugins:

public class PluginLoader {
    private final Class<? extends Annotation> pluginAnnotationType;
    private final String pluginBasePackage;
    private ListableBeanFactory context;

    /**
     * 
     * @param pluginAnnotationType The {@link Annotation} for plugin descriptor types
     * @param pluginBasePackage Base package to look in for plugins
     */
    public PluginLoader(Class<? extends Annotation> pluginAnnotationType, 
            String pluginBasePackage) {
        this.pluginAnnotationType = pluginAnnotationType;
        this.pluginBasePackage = pluginBasePackage;
    }

Most of the work is done in the loadContext() method. Here, all plugins annotated with your plugin annotation are instantiated into an AnnotationConfigApplicationContext:

    private void loadContext() {
        if (context == null) {
            // Create a parent context containing all beans provided to plugins
            // More on that below in the article...
            GenericApplicationContext parentContext = 
                new GenericApplicationContext(providedBeans);
            parentContext.refresh();

            // Create the annotation-based context
            AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
            context.setParent(parentContext);
            
            // Scan for classes annotated with @<PluginAnnotaionType>,
            // do not include standard Spring annotations in scan
            ClassPathBeanDefinitionScanner scanner = 
                new ClassPathBeanDefinitionScanner(context, false);
            scanner.addIncludeFilter(new AnnotationTypeFilter(pluginAnnotationType));
            
            scanner.scan(pluginBasePackage );
            context.refresh();
            
            this.context = context;
        }
    }

The method getPluginDescriptors() returns all plugin beans in the context that implement a specified interface:

    private static <T> Collection<T> getPluginDescriptors(ListableBeanFactory context,
            Class<T> pluginDescriptorType) {
        return context.getBeansOfType(pluginDescriptorType).values();
    }

The getPlugins() method will call loadContext() to find and instantiate all plugins if necessary, and return a collection of plugin instances:

    /**
     * Returns all plugins annotated with the annotation specified in the constructor,
     * and returns all instances having type T. This method can be called multiple times
     * for the same loader if you support multiple plugin descriptor types. The plugin
     * scan will be done only once.
     * 
     * @param <T> Plugin descriptor interface type
     * @param pluginDescriptorType Plugin descriptor interface class
     * @return A {@link Collection} of all plugins implementing the specified plugin interface type
     */
    public <T> Collection<T> getPlugins(Class<T> extensionPointType) {
        loadContext();
        return getPluginDescriptors(context, extensionPointType);
    }

What if your plugins require access to beans in the context they are loaded in? For example an application-provided DataSource, or some sort of resource manager? To achieve this, PluginLoader needs a provideBean() method that allows the hosting application to provide bean instances to plugins:

    /**
     * Provide beans available to plugins.
     * @param beanName Bean name
     * @param bean Instance
     */
    public void provideBean(String beanName, Object bean) {
        if (context != null) {
            throw new IllegalStateException(
                "provideBean must be called before loadContext/getPlugins");
        }
        providedBeans.registerSingleton(beanName, bean);
    }

Plugins can then reference these beans:

@MyPlugin
public class SomePlugin implements MyProviderPlugin {
    @Autowired @Qualifier("resourceManager") ResourceManager resourceManager;
    ...
}

Loading your plugins is as easy as:

    PluginLoader pluginLoader = new PluginLoader(MyPlugin.class);
    Collection<MyProviderPlugin> providers = 
        pluginLoader.getPlugins(MyProviderPlugin.class);

An example usage of the plugin loader would be to inject plugins into your application context somwhere along these lines:

@Configuration
public class MyAppConfiguration {
    ...
    @Bean
    public ProviderRepository providerRepository() {
        PluginLoader pluginLoader = new PluginLoader(MyPlugin.class);
        // Inject another bean reference into the plugin context
        pluginLoader.provideBean("resourceManager", resourceManager());
        Collection<MyProviderPlugin> providers = 
            pluginLoader.getPlugins(MyProviderPlugin.class);
        return new ProviderRepository(providers);
    }
    ...
}
Advertisements
This entry was posted in Java, Spring. Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s