views:

984

answers:

4

There are several mechanisms for module/plugin mechanisms:

Inspired by this dZone article, I had hoped that we could come up with a comparison between competing mechanisms.

For some reason however, this subject inspires a certain amount of religious fanaticism in some, so I would request to maintain the SO tradition of cordiality.

For the same reason, I expect, you very rarely come across anyone who has experience in more than one system.

Therefore, I would like to take a lead from the dZone article and ask, for any Java plugin/module system, how do you do the follow tasks:

  • defining an extension point
  • consuming an extension point
  • write an extension for an extension point.

Any outstanding features/niceties, or glaring problems/warts would also be useful. Necessarily, these should only be written by people who have had experience with the system they are criticising.

From these, it would be easier to synthesise a more accurate comparison between the systems.

Edit:

I'd still like to see answers on:

The following have also been suggested:

For some reason, I really wanted Android to have something that resembled a plugin framework/mechanism. Is this true that it doesn't? Or am I squinting at it perculiarly?

It would be great to hear from some Jigsaw/JSR-294 folks, but since this seems to be in active development, and as yet unreleased, I don't know if this is going to happen.

+5  A: 

OSGi

Defining an extension point A.K.A. defining a service. No upfront work to do.

Consuming an extension point Write a ServiceTrackerCustomizer and construct a ServiceTracker for the classname you're interested in. The customizer gets called any time a service object is registered, modified, or removed. The actual object itself is derived from the service object.

Here is a dzone article on consuming a service.

Registering an extension for an extension point: use the [bundleContext.registerService(String className, Object object, Dictionary attributes)][3] method in your BundleActivator. The activator is called each time the client bundle is started or stopped.


This posts describes a Java only use of registering services, though there is a newer XML based version built on top of this called Declarative Services (DS). This is designed to aid processing of extensions that have not been loaded.

OSGi has built in support for

  • loading and unloading of services, dynamically.
  • versioning of classes,
  • package visibility,

Tooling is good, via the Eclipse PDE, though IMHO, too much is hidden, and con-fused with plugin development. The story for building and testing is not great at the moment, but a number of initiatives are gaining in maturity and popularity:

  • Buckminster
  • Maven
  • Pax-Build
  • Eclipse Ant

Documenting your service is not catered for directly.

OSGi is 1.4 compatible, and so cannot take advantage of annotations and generics which would (IMHO, vastly) improve the mechanism.

jamesh
+1  A: 

Spring makes writing extensions fairly easy:

  • defining an extension point You just define some contract, like implementing an interface or annotating your class with something like @Qualifier: 3.11.3. Fine-tuning annotation-based autowiring with qualifiers. The trick here is to make the beans implementing the extension available to the bean container
  • consuming an extension point You can ask the bean container for all spring beans that implement some marker interface and distinct them by the @Qualifier or any other user-defined property
  • write an extension for an extension point This can be accomplished by implementing the specified contract and then making the implementing bean or beans registered within the bean container through the regular spring mechanism.

Although probably any IoC container could be used, I am only familiar with Spring. Osgi is probably a better solution since it is specifically written for things like plugins. IMHO the main concern when using a plugin mechanism is the lifecycle, ie, deployment, redeployment, etc. Most homebrew solutions only work with compile-time mechanisms; when you get used to runtime reload, you'll never want to get back. For instance, everyone hates when eclipse wants a restart because a plugin was installed.

Miguel Ping
Is this Spring DM? or regular IoC? I like your comment re:runtime-reolads, though I think problems stem from the assumption by the layer above that there is no removal and re-insertion of extensions, i.e. the application is not Observing the extension registry.
jamesh
I'm referring to regular IoC. In Grails for instance, Spring beans can be fully reloaded, however I don't know if this is possible because of Spring or because the Grails guys did some classloader magic.
Miguel Ping
+3  A: 

As a developer on the NetBeans core, I can speak to how its module system deals with extensions. I will not attempt here to describe other aspects of the NB Platform.

There are two basic ways that things can be registered in NetBeans: Java-style services; and the configuration filesystem. Services work very similarly to java.util.ServiceLoader, and in fact form a superset of that functionality. (Added features include optional ordering of services; and masking of unwanted services, generally to override them.) As of NB 6.7, rather than creating a META-INF/services/interfacename file by hand, you can use the new @ServiceProvider annotation, making it very easy to declare implementations. For example, where each package is intended to potentially reside in a separate module:

package spi;
public interface MyService {
    void work();
}
package implementation;
@org.openide.util.lookup.ServiceProvider(service=MyService.class)
public class MyImpl implements MyService {
    public void work() {}
}
package api;
public class MyServices {
    private MyServices() {}
    public static void makeThemAllWork() {
        for (MyService s : org.openide.util.Lookup.getDefault().lookupAll(MyService.class)) {
            s.work();
        }
    }
}

Rather than lookupAll you can use lookupResult to get an object which can both be queried for a list of all current instances, and listened to. If a provider module (MyImpl above) is dynamically enabled or disabled, you will get a notification.

It is also possible to group implementations under arbitrary "paths", a bit like JNDI. Other than using a nondefault namespace, usage is similar:

package implementation;
@ServiceProvider(service=MyService.class, path="offline")
public class MyImpl implements MyService {...}
package api;
/** @param category e.g. "offline" */
public static void makeSomeOfThemWork(String category) {
    for (MyService s : org.openide.util.lookup.Lookups.forPath(category).lookupAll(MyService.class)) {
        s.work();
    }
}

Services are fine for many purposes. But there are two other things you might want, both familiar to users of Eclipse extension points:

  1. Ability to register static data (e.g. XML files in some format) rather than Java objects.
  2. Ability to associate some metadata with each object to facilitate lazy loading.

For these use cases, NB uses the configuration filesystem, available as of NB 6.7 using org.openide.filesystems.FileUtil.getConfigFile. Modules can register virtual "files" associated with them in a directory structure, all the file trees coming from all modules are merged into one tree using an overlay system, and other modules can browse the resulting master tree. The files are lightweight and you can receive notifications of changes in any part of the tree, usually in response to dynamic changes in the list of modules. Furthermore, you can write to the file tree using regular filesystem API calls, and the results (in the form of a diff from the "pristine" state) are persisted to disk as part of the user's settings. Files can also have named attributes which can be of primitive type, or Java objects created on demand.

For registering static data, you simply define your entries in an XML "layer" which is part of your module's source code:

<filesystem>
    <folder name="DocumentTemplates">
        <!-- relative URL points to a file "resume.txt" in the same package in the module JAR -->
        <file name="resume" url="resume.txt">
            <attr name="format" stringvalue="text/plain"/>
        </file>
    </folder>
</filesystem>

Client modules can browse these files and work with their contents very similarly to java.io.File:

for (FileObject template : FileUtil.getConfigFile("DocumentTemplates").getChildren()) {
    if (!"text/plain".equals(template.getAttribute("format"))) continue;
    offerDocumentTemplate(template.getURL());
}

They can also persist user customizations if that makes sense. If resume were selected above, the following would write out NBUSERDIR/config/DocumentTemplates/resume with your customized resume template:

FileObject template = ...;
OutputStream os = template.getOutputStream();
try {
    ...
} finally {os.close();}

It is common that you really wanted to register Java services, but would not want to actually load every service in a folder just to find the right one: doing so would involve too much class loading from modules that might otherwise be dormant. In that case you can use instance files which the NB infrastructure associates with a Java object. You can inspect attributes prior to deciding the load the actual instance. While there are various ways to arrange this using lower-level APIs, a common practice is to first declare the fully general service, then declare a service proxy that can limit excess class loading by taking advantage of some static metadata. For example:

package spi;
public interface DocumentWriter {
    boolean canHandle(String mimeType);
    void write(Document doc, OutputStream out);
}
package api;
public class DocumentWriters {
    public static boolean write(Document doc, OutputStream out) {
        for (DocumentWriter w : Lookup.getDefault().lookupAll(DocumentWriter.class)) {
            if (w.canHandle(doc.getMIMEType())) {
                w.write(doc, out);
                return true;
            }
        }
        return false;
    }
}

The above works but if there are dozens of modules with document writers, you would wind up loading half of them on average just to write a single document. In practice, most document writers will only handle a single MIME type which is determined in advance, so it is silly to load unrelated writers. Without changing either the API or SPI, you can make this more efficient:

package spi;
public class LazyDocumentWriter implements DocumentWriter {
    private final Map<String,?> attrs;
    private DocumentWriter impl;
    private LazyDocumentWriter(Map<String,?> attrs) {this.attrs = attrs;}
    /** factory method for use from XML layers */
    public static DocumentWriter create(Map<String,?> attrs) {
        return new LazyDocumentWriter(attrs);
    }
    public boolean canHandle(String mimeType) {
        return mimeType.equals(attrs.get("mimeType"));
    }
    public void write(Document doc, OutputStream out) {
        if (impl == null) impl = attrs.get("impl");
        impl.write(doc, out);
    }
}

A module can now register a lazy-loaded document writer as follows:

<filesystem>
    <folder name="Services">
        <!-- Services/**.instance are automatically added to Lookup.getDefault: -->
        <file name="my-doc-writer.instance">
            <!-- only consider when DocumentWriter is being looked up -->
            <attr name="instanceOf" stringvalue="spi.DocumentWriter"/>
            <!-- call LazyDocumentWriter.create to make the instance: -->
            <attr name="instanceCreate" methodvalue="spi.LazyDocumentWriter.create"/>
            <!-- this attribute is accessible to the factory method: -->
            <attr name="mimeType" stringvalue="text/plain"/>
            <!-- this one too; MyTextDocWriter loaded iff the value of the attr requested: -->
            <attr name="impl" newvalue="implementation.MyTextDocWriter"/>
        </file>
    </folder>
</filesystem>

Since this registration is verbose and not statically checked for syntax and types, in NB 6.7 the SPI can pair LazyDocumentWriter with an annotation that creates the registration for you automatically during compilation. This is done using JSR 269; there is a helper class so the annotation processor is only a few lines long. So then the implementation module need only write:

package implementation;
@DocumentWriter.Registration(mimeType="text/plain")
public class MyTextDocWriter implements DocumentWriter {...}

The annotation-driven lazy registration style introduced in NB 6.7 was preceded by some prototype work I did on https://sezpoz.dev.java.net/ which is a very simple library that implements the essentials of extension points without any XML or other non-Java metadata for the developer to deal with. The home page gives an example of its usage. While it is at least as easy to use as ServiceLoader, you can use the full syntax of Java annotations to associate metadata with services without necessarily loading the service implementation. No tooling is needed beyond a JSR-269-compliant compiler such as javac and a generic Java code editor.

For NB it was decided for reasons of compatibility with a large existing body of SPIs using the configuration filesystem that annotations should continue to generate the filesystem XML format "under the hood". Unlike SezPoz (so far), NB's API has a full set of features that are sometimes useful when working with services: listening for dynamic changes; ability to mock out the global registries in tests; persistent customizations; optional ordering; etc.

Jesse Glick
Thank you. This is a really good answer.
jamesh
A: 

Neil Bartlett has an OSGi eyed look at OSGi Declarative Services, Apache iPOJO and Spring DM.

jamesh