Skip to content

Service discovery

Joel Håkansson edited this page Nov 13, 2018 · 10 revisions

OSGi and SPI are two technologies that can be used for discovering implementations at runtime. Projects should support both technologies if possible. See below on how to accomplish that.

Introduction

First a very brief introduction to these technologies.

SPI

The SPI is part of the Java API and it is easy to use. It does however have some inherent problems, which is one of the reasons why OSGi came to be in the first place.

Implementations that are to be provided as Java services must be registered in the META-INF/services folder. That folder should contain one file for each SPI interface. The file name must be equal to the interface's canonical name. The contents of each file should be a list of canonical names of implementing classes.

Note that Java 9 uses the module-info file to describe services.

OSGi

To use OSGi services, the application must run in an OSGi container.

All projects here use OSGi-annotations to define service implementations. Hence, only this approach is described below.

The implementation class (which implements a service interface) is annotated with @Component. Dependencies on other OSGi services can be obtained by adding the @Reference annotation on a method with the dependency's interface as argument. This method can be bound automatically with a corresponding method to unregister the implementation (should it become unavailable for some reason). This method's name follows a pattern suggested by the method used to register the implementation, that is to say the method having the @Reference annotation.

The annotations are used to generate xml-files (a.k.a. "declarative services") at build time. These files are placed in the jar's OSGI-INF folder. The files are named after the implementations and specify in them the interfaces they implement and the services they rely on.

Supporting both SPI and OSGi

Supporting both SPI and OSGi is not exactly straight forward, but it can be done reliably using the pattern described below.

1:

Both SPI and OSGi rely on implementations having a public constructor that takes no arguments. The instance created will have no references to other discoverable objects. These must subsequently be set, see below.

2:

Include a method called setCreatedWithSPI() on interfaces which may be discovered by both SPI and OSGi.

OSGi context

If the object is created in an OSGi context, its references (or dependencies) are set using the methods annotated on the implementation. For example:

/**
 * Adds a factory (intended for use by the OSGi framework)
 * @param factory the factory to add
 */
@Reference(cardinality=ReferenceCardinality.MULTIPLE, policy=ReferencePolicy.DYNAMIC)
public void addFactory(EmbosserProvider factory) {
	providers.add(factory);
}

/**
 * Removes a factory (intended for use by the OSGi framework)
 * @param factory the factory to remove
 */
// Unbind reference added automatically from the annotation on addFactory 
public void removeFactory(EmbosserProvider factory) {
	providers.remove(factory);
}

SPI context

In an SPI context it's a bit more complicated (if the goal is to also support OSGi, that is). The methods above are not, and cannot be, in the API. Therefore, they cannot be used in an SPI context since instances created with SPI are only aware of methods provided by the interface. The caller cannot be expected to know what references every possible implementation needs anyway. Fortunately, the caller can know that it is in SPI context (typically by the use of ServiceLoader.load(class)). Therefore, when creating objects using for example ServiceLoader.load(class), the method setCreatedWithSPI() can be called on the created instance to inform it that it itself can use calls to ServiceLoader.load(class) to set up its references (or dependencies) and so on. Note that the instance typically calls newInstance() on concrete classes rather than calling ServiceLoader directly. For example:

public static EmbosserCatalog newInstance() {
	EmbosserCatalog ret = new EmbosserCatalog();
	Iterator<EmbosserProvider> i = ServiceLoader.load(EmbosserProvider.class).iterator();
	while (i.hasNext()) {
		EmbosserProvider ep = i.next();
		// setCreatedWithSPI() allows the implementation to set its references using SPI service discovery,
		// including calls like SomeFactory.newInstance();
		ep.setCreatedWithSPI();
		ret.addFactory(ep);
	}
	return ret;
}

While it would be possible to create a more dynamic solution to this problem, it is important to remember that this pattern only exists in order to support both OSGi and SPI. Therefore, hacking the discovery mechanism for SPI contexts doesn't solve any problem, aside from, perhaps, one of aesthetics.

See also the Java modules plan.

Clone this wiki locally