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:
- Ability to register static data (e.g. XML files in some format) rather than Java objects.
- 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.