Monday, August 29, 2011

java.util.ServiceLoader in OSGi

One of the bigger stumbling blocks for migrating an existing application to OSGi has been java.util.ServiceLoader and its predecessors (the various FactoryFinders that can be found in technologies such as JAXP, JAXRS, etc...).

ServiceLoader is a class provided by the JRE aimed at loading pluggable implementations of something. For those who need a little refreshing of the brain, here's what typical ServiceLoader usage looks like... Let's say you have an SPI called MySPI with a single method doit():
public abstract class MySPI {
    public abstract String doit();
}

For some reason SPI providers are often realized as abstract classes, so that's what I'm doing here too. Obviously using an interface is also possible...
You load the SPI implementations by calling ServiceLoader.load(). To obtain the actual implementation objects you iterate over the result of that call:
ServiceLoader ldr = ServiceLoader.load(MySPI.class);
for (MySPI spiObject : ldr) {
  spiObject.doit(); // invoke my SPI object
}
So why is this such a big problem in OSGi? The problem is that the JRE's ServiceLoader is non-modular by design. It relies on a single classloader that has visibility over everything in your system that can possibly contain an implemetation of the service requested. The load() method as used above uses the ThreadContextClassLoader (TCCL) to find all the ServiceLoader implementations in the system. There are a number of issues with this from an OSGi perspective.
  1. The value of the TCCL is generally not defined in an OSGi context.
  2. The implementations are found by looking for a resource in META-INF/services. Every jar that contains an implementation of a service (e.g. my org.acme.MySPI implementation) should advertise this by putting a META-INF/service/org.acme.MySPI file in its jar file.
  3. Once the above resources have been found, the contents of them are parsed to obtain the class names of implementations, normally these classes reside in an implementation package, something like org.acme.impl.MySPIImpl... ServiceLoader will instantiate the classes found using their no-arg constructor.

Issue 2 clearly poses an issue if this resource is to be obtained through the classloading rules set by OSGi as OSGi wires a package import only to precisely one exporter. This gives great predictability (you know exactly where you're getting a package from) but it doesn't work if you need to get the same resource in the same package from multiple bundles. And even if you were able to get all the META-INF/services resources in your system you would still have the issue that the classes listed in there are generally private implementation classes that you don't want to export outside of a bundle. To load and instantiate them from a consuming bundle you'd also have to import these private implementation packages which would be totally horrible (issue 3).

So how should we do this in OSGi? Clearly the best answer is: use the OSGi service registry. It doesn't suffer from any of the problems ServiceLoader has: no need to expose and import private packges, no need to expose in a single package across multiple bundles. Plus the OSGi Service Registry supports a dynamic service lifecycle: services can come can go, additional suitable services can appear, existing services can get replaced. Furthermore, the OSGi ServiceRegistry supports a rich language of selecting services among possible candidates by using LDAP filters that work over service properties, while ServiceLoader simply gives you a snapshot of all the known service implementations at a particular point in time.

However - you might be looking at some code that you'd like to use as-is. You might not have the luxury of using the OSGi Service Registry right now. Maybe you don't have the source code so you can't modify it or maybe you're just focusing on other things and don't want to modify the code. You just like to use the same code that works with ServiceLoader outside of OSGi, in OSGi...

For this use-case I've started looking at a solution. The solution is based on the premise that you don't want or can't modify the code that uses ServiceLoader. Since this code is almost certainly non-OSGi you will have to turn it into an OSGi bundle by either wrapping it (put the original jar unmodified inside an OSGi wrapper bundle) or by modifying the MANIFEST.MF file to add things like Bundle-SymbolicName and Import-Package statements. Since you're modifying this file anyway, it's not a big deal to add an extra header in there to identify the bundle as a ServiceLoader-user. The header is called SPI-Consumer. The simplest form is:
  SPI-Consumer: *
Which means that this bundle is identified as an SPI consumer with no restrictions. This means that when code such as

  ServiceLoader ldr = ServiceLoader.load(MySPIProvider.class);
is encountered, something special will have to be done. What needs to be done is that the TCCL is set to the bundle class loader of the bundle that contains the implementation of MySPIProvider for the duration of the load() call. It needs to be the classloader of the bundle itself, so it has visibility of the private packages which are needed for ServiceLoader to do its business.

In my implementation, which is available in the Apache Aries project, the consumer byte-code is slightly modified so that for the duration of the ServiceLoader.load() call the TCCL is set to the classloader of the providing bundle(s).

How do we know that a bundle is providing an implementation for use with java.util.ServiceLoader? Similar to the consumer side, this code was most likely not written as an OSGi bundle, so it either needs to be wrapped or its MANIFEST.MF needs to be enhanced with Bundle-SymbolicName etc. Here we can identify the bundle as an SPI provider by adding:
  SPI-Provider: *
This means that all the implementations found in META-INF/services are considered as SPI providers. (You can also provide a subset by enumerating all the SPI names provided, instead of specifying *).

Specifying this header does two things:
  1. It marks the bundle as an SPI provider for use with bundles that are marked as SPI consumer.
  2. It registers the implementations found in META-INF/services in the OSGi Service Registry so that they can be used from there too.
Try it out!
Ok - let's try it out. For that you need:
  • An OSGi Framework - I'm going to use JBoss AS7.
  • An SPI Consumer, Provider and the SPI itself.
  • The Aries SPI-Fly project which implements the functionality associated with the SPI-Consumer and SPI-Provider Manifest headers.
Start the OSGi Framework
I'm taking JBoss AS7 which contains a fully compliant OSGi 4.2 Core Framework. Get version 7.0.1 from here: http://www.jboss.org/jbossas/downloads/
Download and unzip it, then run bin/standalone.sh (or standalone.bat):
...
11:49:45,016 INFO  [org.jboss.as.deployment] (MSC service thread 1-12) Started FileSystemDeploymentService for directory /Users/davidb/jboss/jboss-as-web-7.0.1.Final/standalone/deployments
11:49:45,028 INFO  [org.jboss.as] (Controller Boot Thread) JBoss AS 7.0.1.Final "Zap" started in 1671ms - Started 93 of 148 services (55 services are passive or on-demand)

BTW the AS7 appserver started up in 1671ms on my plain mac!

Get some example bundles
I'm going to use some of the example bundles from the Aries SPI-Fly project...

You need a SPI Consumer. For instance this one that uses the SPI from the Bundle Activator:
public class Activator implements BundleActivator {
    public void start(BundleContext context) throws Exception {
        ServiceLoader ldr = ServiceLoader.load(SPIProvider.class);
        for (SPIProvider spiObject : ldr) {
            System.out.println(spiObject.doit()); // invoke the SPI object
        }
    }

    public void stop(BundleContext context) throws Exception {}
}

I have a separate bundle that contains the SPI itself:
public abstract class SPIProvider {
    public abstract String doit();
}

I used an abstract class here instead of an interface as that seems to be common practise with SPIs used with ServiceLoader.

And I have an SPI Provider bundle with an implementation:
package org.apache.aries.spifly.mysvc.impl2;
import org.apache.aries.spifly.mysvc.SPIProvider;

public class SPIProviderImpl extends SPIProvider {
    public String doit() {
        return "Doing it too!";
    }
}

The provider bundle has the SPI-Provider: * Manifest header in it and the consumer bundle has the SPI-Consumer: * header. 
As the SPI-Fly project hasn't been released yet, these bundles can't be downloaded from Maven yet. You can build them yourself from here: http://svn.apache.org/repos/asf/aries/trunk/spi-fly/spi-fly-examples
You can also download them from these locations where I put my built artifacts:
Add OSGi-ServiceLoader support
The Apache Aries SPI Fly component provides two ways to add ServiceLoader support to an OSGi Framework:
  1. Dynamically by using the OSGi 4.3 Weaving Hooks. With this you can install the above bundles as-is in the OSGi Framework and the treatment of the ServiceLoader.load() calls will happen on the fly.
  2. Statically by transforming the bundle Jar file before installing it in the OSGi framework. This mechanism doesn't need the 4.3 Weaving Hooks.
As not all OSGi frameworks support the Weaving Hooks yet, I'll be showing the static mechanism here. If you want to try the dynamic mechanism see the Aries SPI-Fly docs for more info.
The static mechanism consists of a tool that transforms your bundles inserting the bytecode needed to set the TCCL when ServiceLoader.load() is called. You can build the tool yourself from here: http://svn.apache.org/repos/asf/aries/trunk/spi-fly/spi-fly-static-tool or download the copy that I built from here: spifly-static-tool.jar .

  java -jar spifly-static-tool.jar consumer-bundle.jar
  [SPI Fly Static Tool] Processing: consumer-bundle.jar

When it's finished you'll find a new bundle called consumer-bundle_spifly.jar in the same directory. This bundle has the ServiceLoader.load() calls treated so that the TCCL is set appropriately for it to work.

Use it at Runtime
Our transformed client bundle has an additional dependency on some SPI-fly classes which are used to appropriately set the TCCL once ServiceLoader.load() is called. This bundle can be found here: http://svn.apache.org/repos/asf/aries/trunk/spi-fly/spi-fly-static-bundle and I have created a pre-built version as well: spifly-static-bundle.jar .

Deploy them into your OSGi Framework. AS7 supports a variety of deployment mechanisms, via maven, AS7 Web Console or Command Line, Felix OSGi Console or JMX. For the sake of simplicity I'm using deployment by copying to the standalone/deployments folder here. Note that although deployment by copying is extremely simple, it does require attention to the order as the bundles found in the deployments folder are instantly started.

So I'm deploying:
On the console we'll see various log messages appear, including these from our SPI Consumer:

12:44:27,480 INFO  [stdout] (MSC service thread 1-3) *** Result from invoking the SPI directly:
12:44:27,483 INFO  [stdout] (MSC service thread 1-3) Doing it too!

Which shows that our ServiceLoader Consumer bundle is now working property inside OSGi.

Conclusion
This example uses java.util.ServiceLoader provider and consumer implementations as-is in OSGi. Using the OSGi Service Registry is clearly a nicer solution, but you simply don't always have the option to change the code from using ServiceLoader to using the OSGi ServiceRegistry. That's where OSGi ServiceLoader support may come in useful.

It can already be used today in any compliant OSGi 4.2 Framework, but in that case consumer bundles need to be transformed before they are installed. If you have a 4.3 (or later) framework the transformation can happen on the fly which means that the whole process becomes more user-friendly: just install ServiceLoader support in the Framework and deploy your bundles - no static tool is needed in that case.

ServiceLoader support is being turned in an OSGi Specification scheduled to be part of the upcoming OSGi Enterprise Release (due early 2012). You can read more details about it in RFC 167 in the Early Access Draft here: http://www.osgi.org/download/osgi-early-draft-2011-05.pdf