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.
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:
class.forName
, which caches the class in the caller's ClassLoader
. This prevents the service implementation from being reloaded.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.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:
ServiceLoader.load
calls to a more OSGi-appropriate implementationMETA-INF/services
files as OSGi servicesIn 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.
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.
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
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:
asm
(for byte-code weaving), which can be downloaded from https://repo1.maven.org/maven2/org/ow2/asm/asm-all/4.0/asm-all-4.0.jararies.util
(dependency for the spifly
bundle), which can be downloaded from https://repo1.maven.org/maven2/org/apache/aries/org.apache.aries.util/1.0.0/org.apache.aries.util-1.0.0.jararies.spifly.dynamic
(provides the Service Loader Mediator), which can be downloaded from https://repo1.maven.org/maven2/org/apache/aries/spifly/org.apache.aries.spifly.dynamic.bundle/1.0.0/org.apache.aries.spifly.dynamic.bundle-1.0.0.jarImport 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:
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
:
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]
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
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.
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
:
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
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>