In the fourth of the Modular Java series, we'll cover declarative modularity. We'll describe how we can define components and then hook them up together, without having a programmatic dependency on the OSGi APIs.
The previous instalment, Modular Java: Dynamic Modularity, described how to bring dynamic modularity to an application through the use of services. These are implementations that export one (or more) interfaces that can be discovered dynamically at runtime. Whilst this allows for full de-coupling between client and server, it leads to a question of how (and when) the services start up.
Start ordering
In a fully dynamic system, services can not only come and go as a system runs, they can also start in different orders. Sometimes, this isn't a big problem; regardless of the start ordering between A and B, if no events (or threads) actually occur until the system is in a steady state and is ready to accept events, then it shouldn't matter which service gets started first.
However, there's a number of ways that this simple assumption can be violated. The canonical example is of logging; typically, services will connect to and start writing to a log service during start-up and other operations. If a log service is not available, what happens?
Given that services can dynamically come and go at runtime, clients should be able to cope when the service isn't present. In that case, it might be wise to fallback to another mechanism (like printing output to stdout) or blocking to wait for a service to become available (unlikely to be the right answer for logging systems). However, ensuring that there is a service available before starting would be the ideal solution.
Start levels
OSGi provides a mechanism to control the ordering of bundles at start-up, through the use of start levels. These are based on the concept of UNIX run levels; the system starts at level one, and then monotonically increments until it hits the target start level. Each OSGi container provides a different default target start level; for Equinox, the default is 6, whilst for Felix it is 1.
Start levels can therefore be used to create an ordering between bundles, by putting key bundle services (like logging) into a lower start level than those that require it. However, because there are only a finite number of start levels available, and that installers tend to choose single-digit numbers for start levels, it isn't a strong guarantee that you'll fix problems through start order alone.
The other point worth observing is that bundles in the same start level are started independently (and potentially concurrently), so if you have a bundle which has the same start level as a log service, then there's no guarantees that it will wire up when expected. In other words, start levels are good for solving large problems but not necessarily for all problems.
Declarative services
One solution to this problem is OSGi's declarative services, hereafter referred to as DS. In this approach, components are wired together by an external bundle as and when they become available. Declarative services are wired together as defined in an individual XML configuration file, which declares what services are required (consumed) and provided.
In our last example, we used a ServiceTracker
to acquire, and if necessary, wait for a service to become available. It would be much more useful if we delay creating the shorten
command until the shortening service was available.
DS defines the concept of a component
which is at a finer granularity than a bundle, but a larger granularity than a service (since a component may consume/provide multiple services). Each component has a name, corresponds to a Java class, and may be activated or deactivated by calls to that class' methods. Unlike OSGi Java APIs, DS allows for the component to be developed as a pure Java POJO with no programmatic dependencies on OSGi at all. This has the fringe benefit of also making DS easy to test/mock.
In order to demonstrate the approach, we'll be using our example previously. We'll need two components; one of them will be the shortening service itself, and the other will be the ShortenComand
that invokes it.
The first task is to configure and register the shorten service with DS. Instead of registering the service via the Bundle-Activator
, we can ask DS to register it upon component startup.
So how does DS know to activate or wire this up? Well, we add an entry to the Bundle's Manifest header, which in turn points to one (or more) XML component definition files.
Bundle-ManifestVersion: 2 ... Service-Component: OSGI-INF/shorten-tinyurl.xml [, ...]*
The OSGI-INF/shorten-tinyurl.xml
component definition looks like:
<?xml version="1.0" encoding="UTF-8"?> <scr:component name="shorten-tinyurl" xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0"> <implementation class="com.infoq.shorten.tinyurl.TinyURL"/> <service> <provide interface="com.infoq.shorten.IShorten"/> </service> </scr:component>
When DS processes this component, it has roughly the same effect as doing context.registerService( com.infoq.shorten.IShorten.class.getName(), new com.infoq.shorten.tinyurl.TinyURL(), null );
. A similar declaration will be needed for the Trim()
service, and is included in the source code below.
A single component can provide multiple services under different interfaces if needed. A bundle may also include multiple components, using the same or distinct classes, each of which provides distinct services.
Consuming the service
To consume the service, we need to modify the ShortenCommand
so that it binds to an instance of the IShorten
service:
package com.infoq.shorten.command; import java.io.IOException; import com.infoq.shorten.IShorten; public class ShortenCommand { private IShorten shorten; protected String shorten(String url) throws IllegalArgumentException, IOException { return shorten.shorten(url); } public synchronized void setShorten(IShorten shorten) { this.shorten = shorten; } public synchronized void unsetShorten(IShorten shorten) { if(this.shorten == shorten) this.shorten = null; } } class EquinoxShortenCommand extends ShortenCommand {...} class FelixShortenCommand extends ShortenCommand {...}
Note that unlike last time, this has no dependencies on the OSGi APIs; and it would be trivial to mock an implementation to verify that it worked correctly. The synchronized
modifier ensures that there's no race conditions when the service gets set.
To tell DS that we need an instance of the IShorten
service bound to our EquinoxShortenCommand
component, we need to define what services it requires. When DS instantiates your component (with the default constructor), it will wire up the IShorten
service by invoking the method defined in the bind
attribute; in other words, setShorten()
.
<?xml version="1.0" encoding="UTF-8"?> <scr:component name="shorten-command-equinox" xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0"> <implementation class="com.infoq.shorten.command.EquinoxShortenCommand"/> <reference interface="com.infoq.shorten.IShorten" bind="setShorten" unbind="unsetShorten" policy="dynamic"
cardinality="1..1" /> <service>
<provide interface="org.eclipse.osgi.framework.console.CommandProvider"/>
</service> </scr:component>
As soon as the IShorten
service is available, this component will be instantiated and wired to the service, regardless of the start ordering between this and the other bundles. The explaination of the policy, cardinality and service provide are covered in the next section.
Policies and cardinality
The policy can be either static
or dynamic
. A static
policy will mean that once set, a service doesn't get changed. If the service goes away, the component is deactivated; and if a new service arrives, then a new instance is created and the service re-bound. This is obviously heavier weight than if we can update the service in place.
With the dynamic
policy, when the IShorten
service is changed, DS will invoke the setShorten()
with the new service, and subsequently unsetShorten()
with the old one.
The reason that DS calls the set
before the unset
is to maintain continuity of the service. If a call came in as the service was being replaced, and the unset
was called first, there would be a chance that the shorten
service could be null
transiently. It's also why the unset
method takes an argument, rather than just setting the service to null
.
The cardinality of the service, which defaults to 1..1
, is one of:
- 0..1 Optional, maximum of one
- 1..1 Mandatory, maximum of one
- 0..n Optional, many
- 1..n Mandatory, many
If the cardinality can't be satisfied (for example, it is mandatory there is no shortening service), then the component is deactivated. If many services are required, then the setShorten()
will be called once for each service. Conversely, the unsetShorten()
will be called for each service that goes away.
Not shown here is the ability for the component to do per-instance customisation when it's brought on-line.
In DS 1.1, the
component
element can also have anactivate
anddeactivate
attribute, corresponding to the method which is invoked upon component activation (starting) and deactivation (stopping).
Lastly, this component also provides an instance of the CommandProvider
service. This is an Equinox-specific service which allows console commands to be provided, and was formerly done in the bundle's Activator
. The advantages of this model are that as soon as the dependent services are available, the CommandProvider
service will automatically be published; in addition, the code itself doesn't need to depend on any OSGi APIs.
A similar solution needs to be implemented for the Felix-specific implementation; since as yet, there's no standard for OSGi command shells. There is OSGi RFC 147, which is a work in progress to permit commands being used in different consoles. The source code included has the shorten-command-felix
component definition for completness.
Starting the services
The above allows us to start the bundles for providing (and consuming) the shortening service in any order. Once the command service is started, it will bind to the highest priority shortening service available; or, if that's not specified, the one with the lowest service ranking. Should a higher priority service be started afterwards, we currently don't take into account and continue to use the service we are currently bound to. However, should a service go away, then we'll be re-bound to the remaining highest priority shortening service at that time, without interruption from the client.
In order to run the examples, you'll need to download and install some extra bundles for each platform:
- Felix
- Config Admin (
org.apache.felix.configadmin-1.2.4.jar
) - SCR Declarative Services (
org.apache.felix.scr-1.2.0.jar
)
- Config Admin (
- Equinox:
By now, you should be familiar with installing and starting bundles; but if not, refer to the Static Modularity article. We'll need to install the above bundles, as well as our shortening service. This is how it would look in Equinox, with the bundles in /tmp
$ java -jar org.eclipse.osgi_* -console osgi> install file:///tmp/org.eclipse.osgi.services_3.2.0.v20090520-1800.jar Bundle id is 1 osgi> install file:///tmp/org.eclipse.equinox.util_1.0.100.v20090520-1800.jar Bundle id is 2 osgi> install file:///tmp/org.eclipse.equinox.ds_1.1.1.R35x_v20090806.jar Bundle id is 3 osgi> install file:///tmp/com.infoq.shorten-1.0.0.jar Bundle id is 4 osgi> install file:///tmp/com.infoq.shorten.command-1.1.0.jar Bundle id is 5 osgi> install file:///tmp/com.infoq.shorten.tinyurl-1.1.0.jar Bundle id is 6 osgi> install file:///tmp/com.infoq.shorten.trim-1.1.0.jar Bundle id is 7 osgi> start 1 2 3 4 5 osgi> shorten http://www.infoq.com ... osgi> start 6 7 osgi> shorten http://www.infoq.com http://tinyurl.com/yr2jrn osgi> stop 6 osgi> shorten http://www.infoq.com http://tr.im/HCRx osgi> stop 7 osgi> shorten http://www.infoq.com ...
Once we've installed and started our dependencies, including the shorten
command, it still doesn't show up in the console. It's only when we start the shortening services that the shorten
command is registered.
When the first shortening service is stopped, the implementation automatically fails back over to the second shortening service. When that's stopped, the shorten
command service gets automatically unregistered.
Notes
Declarative Services makes wiring OSGi services easy. However, there are a few points to be aware of.
- The DS bundle needs to be installed and started in order to wire up components. As such, it's typically installed as part of the OSGi framework start-up, such as Equinox's osgi.bundles or Felix's felix.auto.start property.
- The DS often has other dependencies which need to be installed. In Equinox's case, it includes the
equinox.util
bundle. - Declarative Services is part of the OSGi Compendium Specification rather than the Core Specification, so it's often the case that a separate bundle needs to be made available for the service interfaces. In Equinox's case, it's provided by
osgi.services
, but in Felix, the interface is exported by the SCR (Service Component Registry, aka DS) bundle itself. - Declarative Services can be configured with properties. It typically makes use of the OSGi Config Admin service; although this is optionally bound/accessed. Therefore, some parts of the DS requires Config Admin to be running; and in fact, Equinox 3.5 has a bug which requires Config Admin to be started before Declarative Services if it is used. This often requires the use of the start-up properties above in order to ensure the correct dependencies are met.
- The OSGI-INF directory (along with XML files) needs to be included in the bundle, as otherwise DS won't be able to see it. You also need to ensure that the
Service-Component
header is present in the bundle's manifest. - It's also possible to use
Service-Component: OSGI-INF/*.xml
to include all components rather than listing them individually by name. This also permits fragments to add new components to a bundle. - The bind and unbind methods need to be
synchronized
in order to avoid potential race conditions, although using compareAndSet() on anAtomicReference
can also be used to act as a non-synchronized placeholder for the single service - DS components needs no OSGi interfaces, and as such, can be mocked for testing or used in other inversion of control patterns like Spring. However, there are both Spring DM and the OSGi Blueprint service which can be used to wire services up, which is the subject for a future topic.
- DS 1.0 didn't define a default XML namespace; DS 1.1 adds the namespace http://www.osgi.org/xmlns/scr/v1.1.0. If no namespace is present, it assumes DS 1.0 compatibility.
Summary
In this article, we've explored how we can decouple our implementations from the OSGi API, and instead use declarative representations of those components. Declarative Services provides both wiring between components and registering of services, which helps to avoid any start-up ordering dependencies. Furthermore, the dynamic nature means that as our dependent services come and go, so too does our component/service come and go as well.
Finally, whether using DS or manually managed services, they all use the same OSGi service layer in order to communicate. Therefore, one bundle could provide services via a manual method, and another could consume it using declarative services (or vice versa). We should be able to mix and match the 1.0.0
and 1.1.0
implementations and they should transparently work.
Installable bundles (also containing source code):