Java ServiceLoader

The ServiceLoader class in the java.util package (added in Java 1.6) provides a means of acquiring an instance of an interface or abstract class. It is used by a variety of different parts in the JDK, where a single implementation is required that cannot be known in advance, such as JDBC drivers.

The ServiceLoader class provides a static load method that can be used to return a ServiceLoader, which in turn provides an Iterator over all services available:

ServiceLoader<Driver> sl = ServiceLoader.load(Driver.class);
Iterator<Driver> it = sl.iterator();
while (it.hasNext()) {
  Driver driver = (Driver) it.next();
  // do something with driver
}

The implementation class for the driver is found by consulting a text file, located under the META-INF/services/ directory. When looking for implementations for the java.sql.Driver class, the service loader will attempt to find files called META-INF/services/java.sql.Driver. The contents of these files are fully qualified class names of services that implement the specified interface.

The file may also contain comments, and any whitespace or content after # is ignored; for example:

# File is META-INF/services/java.sql.Driver
org.h2.Driver                      # H2 database driver
org.mariadb.jdbc.Driver            # Maria DB driver
org.apache.derby.jdbc.ClientDriver # Apache Derby driver

When the ServiceLoader.load method is called with a Driver.class argument, the three classes will be instantiated and returned in the iterator, in the order that they appear in the file. Any duplicates from this file or from other matching files are filtered from the list.

Problems with ServiceLoader, OSGi, and Eclipse

Although the ServiceLoader provides a general mechanism to return instances of classes based on their interface type, there are the three specific problems that prevent its general use with OSGi:

  • The implementation class is loaded with class.forName, which caches the class in the caller's ClassLoader. This prevents the service implementation from being reloaded.
  • The META-INF/services/ folder is not a package, so it cannot be referred to with the normal Import-Package OSGi semantics. In addition, the package name (directory) can only be exported by a single bundle, so even if this could be used, it would not be possible to bind to more than one provider.
  • The loader for the service is typically taken from the current Thread Context ClassLoader, and in OSGi, the calling class is unlikely to have visibility to the implementation package (and in any case, explicitly importing the implementation class defeats the point of having the class unknown until load time).

To solve these problems, the OSGi Enterprise Specification Release 5 provides the Service Loader Mediator, implemented by the Apache Aries SPI-Fly bundle. This provides the following two key features:

  • For consumers, it uses bytecode weaving that can dynamically rewrite ServiceLoader.load calls to a more OSGi-appropriate implementation
  • For providers, it automatically registers implementations defined in any META-INF/services files as OSGi services

In both cases, the consumer and producer need to opt-in explicitly through the use of entries in the MANIFEST.MF to ensure that bundles are weaved (or not) on demand. The weaving bundle org.apache.aries.spifly.dynamic.bundle also needs to be installed and started prior to any consumers starting.

Note

The org.apache.aries.spifly.dynamic.bundle needs org.apache.aries.util to resolve, and org.objectweb.asm-all to perform the bytecode weaving. It is possible to pre-weave a bundle using org.apache.aries.spifly.static.bundle as documented on the home page at http://aries.apache.org/modules/spi-fly.html.

Creating a service producer

Create a plug-in project called com.packtpub.e4.advanced.loader.producer. This does not need an Activator and will be a standard OSGi bundle that targets Standard OSGi.

Create a class in the com.packtpub.e4.advanced.loader.producer package called HelloWorldRunnable that implements Runnable:

package com.packtpub.e4.advanced.loader.producer;
public class HelloWorldRunnable implements Runnable {
  public void run() {
    System.out.println("Hello World");
  }
}

Create a META-INF/services/java.lang.Runnable file with the following content in order to register it as a service for the ServiceLoader:

com.packtpub.e4.advanced.loader.producer.HelloWorldRunnable

This is enough for the ServiceLoader to find it with ServiceLoader.load, but in order for it to work in an OSGi runtime, the bundle needs to have additional OSGi metadata. Add the following to the META-INF/MANIFEST.MF file:

Require-Capability:
 osgi.extender;
  filter:="(osgi.extender=osgi.serviceloader.registrar)"

This expresses a dependency on the OSGi Service Loader Mediator (provided by SPI-Fly). If this dependency is missing, the bundle will fail to resolve.

By default, all services under the META-INF/services/ directory will be made available. If a single service type should be exported, it can be expressed with Provide-Capability on the osgi.serviceloader:

Provide-Capability:
 osgi.serviceloader;osgi.serviceloader=java.lang.Runnable

Multiple instances of the same interface need no extra configuration lines; all of the instances in the java.lang.Runnable file will be exported. If there are multiple service types (in other words, multiple files under the META-INF/services/ directory), they can be represented as follows:

Provide-Capability:
 osgi.serviceloader;osgi.serviceloader=java.lang.Runnable,
 osgi.serviceloader;osgi.serviceloader=java.util.Comparator

Downloading the required bundles

To run the producer, some prerequisite bundles must be acquired. These can be downloaded from Maven Central or from the book's GitHub repository at https://github.com/alblue/com.packtpub.e4.advanced/.

The necessary bundles are as follows:

Import these into the Eclipse workspace by navigating to File | Import | Plug-in Development | Plug-ins and Fragments, and then choose the directory that the prerequisite bundles have been downloaded into, as illustrated in the following screenshots:

Click on Next and select all of the available bundles by choosing Add All, followed by Finish:

Running the producer

To run the producer, create a new Launch Configuration by navigating to Run | Run Configurations … menu. Click on OSGi Framework and hit the New button to create a new configuration called ServiceLoader Producer Only:

Running the producer

Add the following bundles from the Workspace:

  • com.packtpub.e4.advanced.loader.producer
  • org.apache.aries.spifly.dynamic.bundle
  • org.apache.aries.util
  • org.objectweb.asm

Add the following bundles from the Target Platform:

  • org.apache.felix.gogo.command
  • org.apache.felix.gogo.runtime
  • org.apache.felix.gogo.shell
  • org.eclipse.equinox.console
  • org.eclipse.osgi

Run the framework by clicking on Run.

Using the console, look at the producer bundle. It should declare that it has registered the Runnable instance as an OSGi service:

osgi> ss | grep producer
4 ACTIVE com.packtpub.e4.advanced.loader.producer_1.0.0.qualifier
osgi> bundle 4
com.packtpub.e4.advanced.loader.producer_1.0.0.qualifier [4]
 Id=4, Status=ACTIVE
 "Registered Services"
 {java.lang.Runnable}={
  .org.apache.aries.spifly.provider.implclass=
  com.packtpub.e4.advanced.loader.producer.HelloWorldRunnable,
  serviceloader.mediator=7, service.id=46}

In this case, the serviceloader.mediator service property is 7, and bundle 7 is the SPI-Fly implementation:

osgi> bundle 7
org.apache.aries.spifly.dynamic.bundle_1.0.0 [7]

Creating a service consumer

Create another plug-in project called com.packtpub.e4.advanced.consumer, this time with an Activator that looks like the following code:

package com.packtpub.e4.advanced.loader.consumer;
import java.util.ServiceLoader;
import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;
public class Activator implements BundleActivator {
  public void start(BundleContext context) throws Exception {
    ServiceLoader<Runnable> sl=ServiceLoader.load(Runnable.class);
    Runnable runnable = sl.iterator().next();
    runnable.run();
  }
  public void stop(BundleContext context) throws Exception {
  }
}

When the bundle starts, the Activator will look for implementations of the Runnable interface, and then run them. The iterator.next method will fail if there are no implementations available, which will prevent the consumer bundle from starting if a Runnable instance cannot be found.

When run, the bundle fails because the client has not been processed to hook the ServiceLoader.load call to look for OSGi specific services.

To fix this, the consumer bundle needs to register the following as a generic capability in the META-INF/MANIFEST.MF file:

Require-Capability: 
 osgi.extender;
  filter:="(osgi.extender=osgi.serviceloader.processor)"

This ensures there is a processor present, and calls to ServiceLoader.load will be replaced with a call to an appropriate OSGi handler routine. Now, when the client bundle is started, Hello World should be printed out when the framework starts up.

As with the producer, it is possible to constrain the consumer such that it only allows lookup for implementations of a specific interface:

Require-Capability:
 osgi.extender;
  filter:="(osgi.extender=osgi.serviceloader.processor)",
 osgi.serviceloader;
  filter:="(osgi.serviceloader=java.lang.Runnable)";
  cardinality:=multiple

Also, as with the producer, if multiple services are required, then these can be added by adding additional osgi.serviceloader capability requirements.

The filter should not be used to represent a disjunction or a conjunction of filters, which would either say "An instance that satisfies either X or Y" or "An instance that satisfies both X and Y."

Instead, there needs to be two separate requirement constraints, the bundle needs X and the bundle needs Y. For example, to require both a Runnable and a List:

Require-Capability:
 osgi.extender;
  filter:="(osgi.extender=osgi.serviceloader.processor)",
 osgi.serviceloader;
  filter:="(osgi.serviceloader=java.lang.Runnable)";
  cardinality:=multiple
 osgi.serviceloader;
  filter:="(osgi.serviceloader=java.util.List)";
  cardinality:=multiple

Tip

Should the bundle import and export individual constraints for each service? An argument against this is that it makes the bundle manifest more complex, and when adding additional services, the manifest needs to be updated as well.

On the other hand, if entries are added, then it is possible for a resolution tool to determine whether this code requires a Runnable instance, and this requires finding a bundle that explicitly provides a Runnable instance.

If there is only one service that is exposed or used by a bundle, having additional requirement constraints can be a good way of documenting the available services.

Running the consumer

To run the consumer, create a new Launch Configuration by navigating to Run | Run Configurations … menu. Click on OSGi Framework and hit the New button to create a new configuration called ServiceLoader Producer And Consumer:

Running the consumer

Add the following bundles from the Workspace:

  • com.packtpub.e4.advanced.loader.consumer (start level 3)
  • com.packtpub.e4.advanced.loader.producer (start level 2)
  • org.apache.aries.spifly.dynamic.bundle (start level 1)
  • org.apache.aries.util
  • org.objectweb.asm

    Note

    In this case, the producer is started first so that when the consumer starts, there is a Runnable instance to acquire. The spifly bundle needs to be started before the consumer so that it has the chance to rewrite the consumer's loading by replacing calls to ServiceLoader with the appropriate OSGi calls.

Add the following bundles from the Target Platform:

  • org.apache.felix.gogo.command
  • org.apache.felix.gogo.runtime
  • org.apache.felix.gogo.shell
  • org.eclipse.equinox.console
  • org.eclipse.osgi

Run the framework by clicking on Run. It should display Hello World in the console as the client bundle is activated and acquires the registered producer service:

Hello World
osgi>

Note

Unlike the standalone producer service, start levels are needed in this case because the consumer runs its service loader code at bundle start-up in the Activator class.

Depending on explicit start-ordering is not good practice in an OSGi runtime, and generally this represents a code smell.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset