Tuesday, February 19, 2013

java.util.ServiceLoader in OSGi

When migrating existing projects to OSGi, generally the biggest issue is the modularization of the code. Non-modular projects might have built up dependencies over time that you weren't aware of, with the risk of creating a spaghetti-like dependency chain in the end. Migrating to OSGi modularity gets rid of that spaghetti. However there are sometimes other challenges, one of which can be getting java.util.ServiceLoader to work inside OSGi.

ServiceLoader was introduced in Java 6 to generalize the various forms of 'FactoryFinder' patterns that were in existence throughout the JRE. It provides a basic plug-in model where implementations of a class (subclasses) or an interface can advertise themselves via a file that has the name of that interface (or class) in the META-INF/services directory of a Jar file. When looking up implementations via ServiceLoader.load(someClass.class), ServiceLoader scans all the Jars visible to the Thread Context Classloader for the associated file in META-INF/services and then uses a zero-arg constructor to instantiate the classes found.

There are a number of issues with this for a modular environment. First of all, ServiceLoader is non-modular by design in that it expects visibility of all Jars in the system, as they can all potentially contain an implementation. Additionally, depending on the Thread Context Classloader is also a questionable design decision as that can only really work well in situations where there is only 1 classloader (e.g. the system classpath) or in situations where all threads are managed by a container. In OSGi this is clearly not the case, as OSGi bundles are free to create threads and an OSGi system typically contains many classloaders.
Note that ServiceLoader also has a load(Class, ClassLoader) API which does not suffer from the issues described above.
Still we want to be able to make existing libraries that rely on ServiceLoader work in OSGi. The OSGi Enterprise R5 specs contain the ServiceLoader Mediator specification which addresses this issue.

ServiceLoader provider
Let's first look at the provider side. An obvious thing to do with bundles that contain META-INF/services resources is to register them as OSGi Services so that OSGi-aware consumers can simply use them from the OSGi Service Registry. So you'll get that with an implementation of this spec. For example take the following provider bundle where the org.acme.MySpiImpl class implements the foo.bar.MySPI interface. In this bundle,

the META-INF/services/foo.bar.MySPI file contains:
  org.acme.MySpiImpl

META-INF/MANIFEST.MF contains:
  Require-Capability: osgi.extender;
    filter:="(osgi.extender=osgi.serviceloader.registrar)"
  Provide-Capability: osgi.serviceloader;
    osgi.serviceloader=foo.bar.MySPI

The ServiceLoader mediator is controlled through the OSGi Manifest via generic capabilities and requirements. This mechanism was introduced in the OSGi 4.3 core specification. Basically what the two lines in the manifest state is that a Service Loader registrar extender is required and that the bundle provides the osgi.serviceloader capability for foo.bar.MySPI.

Once the provider bundle has been extended by the ServiceLoader mediator, you can find its corresponding service present in the OSGi Service Registry:

  g! inspect capability service 8
  org.acme.provider.bundle [8] provides:
  -------------------------------------------
  service; foo.bar.MySPI with properties:
    service.id = 17
    serviceloader.mediator = 6

you can identify services that are registered this way by the fact that the serviceloader.mediator property is present.

The OSGi Service Registry provides a much richer service environment than the functionality provided by java.util.ServiceLoader as besides the ability to plug-in service implementations it supports a dynamic lifecycle (services can come and go) and it has its rich service lookup query mechanism - plus it is designed to work in a modular environment.

If you can change the client-side code that uses the provider to consume it from the OSGi Service Registry, you have all you need at this point.

ServiceLoader consumer
However, in some cases you may not have access to the client-side code that is using ServiceLoader.load(). Maybe you're using an existing library that was written in another project. Maybe you're using a commercial library that you can't modify. For these cases the Service Loader Mediator specification also describes a way to get existing ServiceLoader.load() consumer code to work. While implementations can implement this in any way they like, the Reference Implementation in Apache Aries (SPI Fly) makes this work by keeping track of the providers and then, using byte code instrumentation, set the Thread Context ClassLoader to provide visibility of the appropriate provider jars for the duration of the ServiceLoader.load() call in the consumer bundle. Here's an example:

The consumer may contain code similar to:
  ServiceLoader<MySPI> ldr = ServiceLoader.load(MySPI.class);
  for (MySPI spiObject : ldr) {
    spiObject.doit(); // invoke the SPI object
  }

In the META-INF/MANIFEST.MF, the consumer bundle has:
  Require-Capability: osgi.extender;
      filter:="(osgi.extender=osgi.serviceloader.processor)",
    osgi.serviceloader;
      filter:="(osgi.serviceloader=foo.bar.MySPI)";
      cardinality:=multiple

This may look like a mouthful, but it's basically two generic OSGi requirements stated in a single Require-Capability header. The consumer requires the osgi.serviceloader.processor extender. This extender is responsible for ensuring that the ServiceLoader.load() call has the Thread Context Classloader set. The consumer also requires the osgi.serviceloader capability for foo.bar.MySPI. This capability is provided by the provider bundle and effectively instructs the extender what to set the TCCL to.

As with the provider, the consumer needs to opt in to the process by specifying the requirement headers in the OSGi manifest, however you don't need to change the actual client side code. If you don't want to modify the original Jar file at all you can wrap it as-is in an OSGi bundle, and provide the additional Manifest header in the wrapped bundle. Or you can simply add the additional manifest header to the existing Jar, whichever you prefer.

The Service Loader Mediator specification helps with migrating existing libraries that use java.util.ServiceLoader to OSGi and is available since June 2012 as part of the Enterprise R5 release. The Reference Implementation at Apache Aries is now released at version 1.0. For the consumer side, Aries comes in 2 variants: one that uses static weaving: you run a command-line tool on the consumer jar which then produces a new jar that works in OSGi. The other variant uses dynamic weaving, which means that you can install your consumer bundle as-is and have it processed at run-time in the OSGi Framework. More details on the RI can be found here: http://aries.apache.org/modules/spi-fly.html

As with any OSGi Specification, you can always check this Wikipedia page for additional implementations: http://en.wikipedia.org/wiki/OSGi_Specification_Implementations

5 comments:

  1. Hi,

    Can we use fragment bundle if the service is known to be consumed only by one bundle ?

    Thanks

    ReplyDelete
    Replies
    1. It might work, but I would not recommend it. In general it's better to avoid fragments if there are solutions that use ordinary bundles.

      Delete
  2. Is is it possible that the service type definition and the consumer reside in same bundle, OSGi spec (OSGi Enterprise 5.0.0, 133.5.3) discourages this but ... my legacy code is legacy code.
    When they are in different bundle it works perfectly !

    ReplyDelete
  3. I think that having them in the same bundle should work. I guess you're using the Aries implementation? Maybe you can file a bug for this in the 'SPI Fly' component at https://issues.apache.org/jira/browse/ARIES

    ReplyDelete
  4. It works ... I had an issue with maven ... sorry for the noise.

    BTW I think I have found an (real) issue in weaving https://issues.apache.org/jira/browse/ARIES-1321 :)
    AFAIU the spi-fly weaver expect only to find LDCInstruction.
    Thanks a lot.

    ReplyDelete