Extension/Service/Plugin mechanisms in Java
Since I started to deep dive into OSGi I was wondering more and more how frameworks that have some way of extension mechanism, e.g. Apache Camel where you can define your own endpoint or the Eclipse IDE with its plugins, handle finding and instantiating extensions. I remember very well a presentation from the JAX 2013, it was by Kai Tödter, where he showed the combination of Vaadin and OSGi. While the web app was running he could add and remove menu entries, just by starting and stopping the bundles.
For a while now I have taken a look at several approaches on how to create an extensible application and you can find resources for every single method. I want to give a medium sized (not short ;)) overview here of the different ways I know to make a Java application extensible. Also, I will add a list of advantages and disadvantages, from my point of view, to each method. For every method I try to give a simple example.
To avoid confusion, when I write about the advantages and disadvantages, I will write from the point of view, as if you want to provide this extension mechanism in your framework, not from the API consumer point of view.
Passing the object
This is the most obvious method. The framework defines a method which takes the SPI interface and you simply pass the object. Camel, next to other methods, makes use of this (example taken from the Camel FAQ):
CamelContext context = new DefaultCamelContext();
context.addComponent("foo", new FooComponent(context));
Internally, Camel doesn't do much magic (code taken from Camel on GitHub).
public void addComponent(String componentName, final Component component) {
ObjectHelper.notNull(component, "component");
synchronized (components) {
if (components.containsKey(componentName)) {
throw new IllegalArgumentException("Cannot add component as its already previously added: " + componentName);
}
component.setCamelContext(this);
components.put(componentName, component);
for (LifecycleStrategy strategy : lifecycleStrategies) {
strategy.onComponentAdd(componentName, component);
}
// keep reference to properties component up to date
if (component instanceof PropertiesComponent && "properties".equals(componentName)) {
propertiesComponent = (PropertiesComponent) component;
}
}
}
Every component has to have an unique name and is somehow bound to a lifecycle. Removal of a component is also possible, but has to be made somewhere from the user code.
Advantages
- Easy and straightforward
- No need for an additional framework
- Compiler checks for the correct interface
Disadvantages
- Access to central class (the plugin/service/component holder) is necessary
- Allowing changes during the runtime is possible but complicated, since it has to be assured the component is removed everywhere
- Your framework has to take care of the whole component lifecycle and any additional requirements it enforces
Interface and Reflection
This method is used quite often (basically it is also how the ServiceLoader works, see next section) and you can find it with small variances. The differences are where and how exactly interface and implementation name reach the application. Placing them somewhere inside a properties file or passing them to the framework during startup are most common. The implementation is then instantiated using reflection. Creating a context with an InitialContextFactory
works like this e.g.:
Properties env = new Properties();
env.put(Context.INITIAL_CONTEXT_FACTORY,
"org.jboss.naming.remote.client.InitialContextFactory");
Advantages
- Easy and straightforward
- No need for an additional framework
- No need to provide central class (in properties file approach)
Disadvantages
- No type safety (if text based)
- Your framework has to take care of the whole lifecycle and any additional requirements it enforces
- Check for correct wiring only during runtime (if text based, check either at startup or when the code is being called, where the former is better than the latter)
java.util.ServiceLoader
Frameworks using the java.util.ServiceLoader
can also be found quite often. What the ServiceLoader does is, it uses during runtime a ClassLoader and checks the META-INF/services directory for a text file, whose name equals the passed interface (SPI) name and then reads the class name inside that file. Then it instantiates the class via Reflection. All the magic happens in the LazyIterator
inside the ServiceLoader class (see OpenJDK). Basically, it's just reading a file and instantiating the object. E.g. Camel and HiveMQ use this method.
Advantages
- Easy and straightforward
- ServiceLoader is part of JDK
- No need for an additional framework
Disadvantages
- No lifecycle
- Class has to provide standard constructor
- Support for runtime changes must be implemented (as mentioned here)
- Check for correct wiring only during runtime (the filename or the string inside the file could be wrong)
(Eclipse) Extension Points
Picture under BSD license, see here
As far as I know the concept of Extension Points never got popular outside Eclipse, although it is possible to include them in every application. To achieve loose coupling the definition of places where you can add your plugin and the plugins themselves is extracted into XML files.
To define an extension point you need something like this:
<?xml version="1.0" encoding="UTF-8"?>
<?eclipse version="3.4"?>
<extension-point
id="de.blogspot.wrongtracks.FooService"
name="FooService"
schema="schema/de.blogspot.wrongtracks.FooService.exsd"/>
The extension provider then has to define an appropriate extension for that point:
<?xml version="1.0" encoding="UTF-8"?>
<?eclipse version="3.4"?>
<plugin>
<extension
point="de.blogspot.wrongtracks.FooService">
<implementation
class="com.example.impl.FooServiceImpl"
id="com.example.impl.FooServiceImpl"
name="FooServiceImpl">
</implementation>
</extension>
</plugin>
I got to admit, that I am not completely sure how exactly you can integrate the extension points, but I guess you will need quite a lot from the basic Eclipse runtime. There is a blog post, which explains how you can use extension points without depending on OSGi.
Advantages
- Extensions can be added during runtime
- Good tool support inside Eclipse
- Wrong wiring only affects single extension
- Loose coupling (more or less, since the extensions depend on the extension point id)
Disadvantages
- Dependencies to Eclipse
- Overhead from the Eclipse platform (I actually cannot prove this point but I assume there must be a considerate overhead involved in comparison to the previous methods)
- Check for correct wiring only during runtime
Spring XML
The Spring framework tried to find a way for loosely coupled components long before CDI, as we know it today, appeared. Their solution was an XML file in which the different classes are being wired together (I am well aware of the fact that nowadays there are also other ways, but since they are also based on annotations they don't differ enough from CDI as that I'll give them an own paragraph). In the basic XML file you define all your beans and Spring will take care of the instantiation. It is also possible to distribute the configuration among several XML files. A very simple example (taken and modified from the Spring documentation) looks like this:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="accountDao"
class="org.springframework.samples.jpetstore.dao.jpa.JpaAccountDao">
</bean>
<bean id="petStore" class="org.springframework.samples.jpetstore.services.PetStoreServiceImpl">
<property name="accountDao" ref="accountDao"/>
</bean>
</beans>
If you want to provide your users a way to add their services/plugins to the framework, you'll have to provide a setter method where the users can add their object. E.g. like this (taken from camunda documentation):
<bean id="processEngineConfiguration" class="org.camunda.bpm.engine.spring.SpringProcessEngineConfiguration">
...
<property name="processEnginePlugins">
<list>
<bean id="spinPlugin" class="org.camunda.spin.plugin.impl.SpinProcessEnginePlugin" />
</list>
</property>
</bean>
Advantages
- Spring is lightweigth
- Lifecycle support from Spring
Disadvantages
- XML needs to be maintained
- No auto detection, users have to write the XML when they want to add something
- The Spring IoC container is needed
- Correct wiring is only checked at startup
OSGi Services
OSGi was created embracing runtime changes and bundles dynamically providing and removing their services. With this in mind OSGi strongly supports applications being extended by services, provided by different bundles. The simplest approach is to implement a ServiceListener
or a ServiceTracker
. Both should be created on bundle start and they will react when a new implementation of the service appears. A ServiceListener
can be as simple as this (taken from the Knoplerfish tutorial):
ServiceListener sl = new ServiceListener() {
public void serviceChanged(ServiceEvent ev) {
ServiceReference sr = ev.getServiceReference();
switch(ev.getType()) {
case ServiceEvent.REGISTERED:
{
HttpService http = (HttpService)bc.getService(sr);
http.registerServlet(...);
}
break;
default:
break;
}
}
};
String filter = "(objectclass=" + HttpService.class.getName() + ")";
bc.addServiceListener(sl, filter);
Where bc is a BundleContext
object. And a ServiceTracker
can be used like this:
ServiceTracker<HttpService,HttpService> serviceTracker = new ServiceTracker<HttpService, HttpService>(bc, HttpService.class, null);
serviceTracker.open();
There are more elegant ways to get hold of an OSGi service using Blueprint, Declarative Services or the Apache Felix Dependency Manager but the ServiceListener
is the basic way.
Advantages
- OSGi lifecycle support
- Changes during runtime "encouraged" ;)
- Compiler checks wiring (not for the
ServiceListener
but for the rest) - Problems with services are restricted to single bundle
Disadvantages
- You have to buy the whole OSGi package: imports, exports, bundles and everything
- Having the full OSGi lifecycle makes the world more complicated since every service can disappear at every moment
Note about PojoSR/OSGi Light
Since the biggest disadvantage of OSGi is that you have to get the whole package, I want to mention here another approach, which is called PojoSR or OSGi Light. The goal of it is to give you the OSGi service concept without the rest that comes with OSGi. Unfortunately, I could not find much documentation about it and the activity around this project seems to be very low at the moment. There is an article here and the PojoSR framework itself. Also, it looks like PojoSR is now a part of Apache Felix called "Connect", but its version is 0.1.0. So if anyone of you knows more about it, please let me know.
CDI
Contexts and Dependency injection was a big step for Java EE, allowing developers to write more loosely coupled code. The CDI container takes care of automagically wiring the different parts together. The developer only has to use the correct annotations. Depending on which CDI beans are present at runtime, concrete implementations can be changed without changing the code that uses them. When trying to use a class the basic injection looks like this:
@Inject private MyServiceInterface service;
If there is need to get all of the implementations (which we actually want here), then the class Instance
must be used:
@Inject @Any private Instance<MyServiceInterface> services;
Since Instance
is an Iterable
a simple for-each loop can be used to access all the objects. Alternatively the select()
method can be used to further specify requirements.
Advantages
- Compiler checks for correct type
- CDI container checks correct wiring at startup
- Part of JEE standard but can also be used without application serve (use a JSR-330 implementation like Guice or HK2)r
- CDI lifecycle support
Disadvantages
- A CDI container is needed
- Changes during runtime are not possible
- Annotatiomania (at least if you don't watch out)
Summary
As you can see many different frameworks/methods evolved in the Java ecosystem. Every single one with its specific advantages and disadvantages. I think we can summarize the different extension mechanisms as three types (with their members):
- String and well-known location ("Interface and Reflection", "ServiceLoader", "(Eclipse) Extension Points", "Spring XML")
- Programmatic wiring ("Passing the object", "Interface and Reflection", "OSGi Services")
- Classpath scanning ("CDI")
Of course the three types are not exclusive. You may provide your users more than one way and let them choose. Also CDI is not exactly the only framework that uses classpath scanning. Spring with its two other ways for configuring the IoC container relies on that method, too.
I hope this article provides an good and sufficient overview of the different methods on how to create an extensible framework. Choosing the right one will make your users surely happy. If you know another method, which I forgot, please let me know, I will gladly add it here.
Please note that the lists of advantages and disadvantages are based on my reasoning. I tried to be objective but like every programmer I have my favorites and my experiences with the frameworks that may make me a little bit biased.
Subscribe to my newsletter
Read articles from Ronny Bräunlich directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by