As we have seen in the previous parts, the service registry provides a very simple and flexible mechanism to establish the relationships between the modules of a software system (provided as bundles). The drawback of its simplicity is that it requires a relatively large effort for individual modules to manage their service dependencies.
A somewhat different approach to managing services is the use of “service components”. The basic ideas have been described in the article “Automating Service Dependency Management in a Service-Oriented Component Model”. Read it now. It’s a very good introduction into the topic1.
Service components supply the information required for managing the service dependencies. The management functions that evaluate this information are implemented in an independent component (the “dependency manager” or “service component runtime”) that is deployed in an OSGi framework in addition to the service components. All dependency managers work according to the same basic pattern. They watch for bundle state changes, either by monitoring the framework or by requiring a bundle to notify the manager about its start explicitly in its bundle activator. Once the bundle has been started, the dependency information is retrieved. When all the required (i.e. non-optional) services are available, some startup-method of the service component is invoked. (And, of course, there is a shutdown-method, which is invoked if one of the required services goes away.)
One of the oldest and at the same time still recent solutions to dependency management is the Felix Dependency Manager (DM). It has its origins in 2004, but has undergone a major overhaul in 2019. The DM supports two mechanism for supplying the dependency information: programmatically or by using annotations. This provides a good entry point into the topic and that’s why I’ll show you the basics in the next sub-sections, although “OSGi Declarative Services” (see below) is the predominantly used solution.
When using Felix DM programmatically, you have to provide a bundle activator that extends the abstract class DependencyActivatorBase and implements the init
-method. This method tells the manager about the service requirements (and about provides services) using the dependency manager’s API.
public class Activator extends DependencyActivatorBase {
public void init(BundleContext context, DependencyManager manager)
throws Exception {
manager.add(
createComponent() // Create a new service component as instance...
.setImplementation(HelloWorld.class) // ... of the HelloWorld class.
.add( // Add to the service component ...
createServiceDependency() // ... a dependency on ...
.setService(LogService.class) // ... the LogService service ...
.setRequired(true) // ... but don't start the instance
// before the LogService is available.
)
);
}
}
The adapted version of your component class looks like this:
public class HelloWorld extends Thread {
private volatile LogService logService;
@Override
public void run() {
System.out.println("Hello World!");
while (!isInterrupted()) {
try {
logService.log(LogService.LOG_INFO, "Hello Word sleeping");
sleep (5000);
} catch (InterruptedException e) {
break;
}
}
}
}
To get this working, we have to add the DM to the sets of “build” and “run” bundles.
During runtime, the DM has additional required dependencies on the OSGi
“Configuration Admin” and the “Metatype” services. So we have to add those as
well. You can do this using the GUI (see below), of course. What you end up
with are entries in the bnd.bnd
similar to this:
-buildpath: \
osgi.cmpn;version=4.3.1,\
osgi.core;version=4.3.1,\
org.apache.felix.dependencymanager;version=4.4.0
-runbundles: \
org.apache.felix.log,\
org.apache.felix.gogo.command,\
org.apache.felix.gogo.runtime,\
org.apache.felix.gogo.shell,\
de.mnl.osgi.log.fwd2jul,\
org.apache.felix.dependencymanager,\
org.apache.felix.configadmin,\
org.apache.felix.metatype
Run the modified bundle with the enhanced configuration and – it works, i.e. we get the “Hello World!” and the log messages. This is nice to see, but where’s the magic, I mean, what started our thread? As it happens, the methods invoked by the DM on a service component are
init
,start
,stop
anddestroy
.Except for the first, these methods are all defined by the Thread
class,
which our component inherits from, and this is why our component works.
Of course, since the stop
and destroy
methods are deprecated (and for
good reasons), we should override them with proper implementations for
anything beyond this simple example2.
The log service has been made available by the DM in the attribute logService
by dependency injection.
The details can all be found in the DM
documentation.
When it comes to optional services, DM supports a nifty feature known as
“Null Object”. Try
it out: change the parameter of setRequired
in the activator’s init
-method
to false
and remove the log service bundle from the set of run bundles.
Our component continues to work, despite the fact that we should get a
NullPointerException
when trying to invoke the log
-method
(logService.log(...)
), since there cannot possibly be a log service available.
If an optional dependency is not available, DM injects an instance of class
Proxy
that simply handles all method invocations by doing nothing (except
for returning a “zero” value matching the method’s return type).
Of course, it’s not guaranteed that this “dummy service implementation”
won’t cause trouble in the invoking code3. But (as in the case of
the log service) this feature can sometimes simplify the code because you
don’t have to check whether an optional service is actually available.
An easier way to specify service related information is the use of annotations. You can do this in the component class like this:
package io.github.mnl.osgiGettingStarted.simpleBundle;
import org.apache.felix.dm.annotation.api.Component;
import org.apache.felix.dm.annotation.api.ServiceDependency;
import org.apache.felix.dm.annotation.api.Start;
import org.apache.felix.dm.annotation.api.Stop;
import org.osgi.service.log.LogService;
@Component(provides={})
public class HelloWorld implements Runnable {
@ServiceDependency(required=true)
private volatile LogService logService;
private Thread runner;
@Start
public void start() {
runner = new Thread(this);
runner.start();
}
@Stop
public void stop() {
runner.interrupt();
try {
runner.join();
} catch (InterruptedException e) {
logService.log(LogService.LOG_WARNING,
"Could not terminate thread properly", e);
}
}
@Override
public void run() {
System.out.println("Hello World!");
while (!runner.isInterrupted()) {
try {
logService.log(LogService.LOG_INFO, "Hello Word sleeping");
Thread.sleep (5000);
} catch (InterruptedException e) {
break;
}
}
}
}
Note that we don’t need an activator any more. The annotations specify the same information as the API calls in the previous example and should be self-explanatory4, maybe with the exception of the @Component
annotation’s parameter “provides
”. All interfaces directly implemented by a class annotated as @Component
are by default assumed to be interfaces of services provided by the component. (Using the API, declaring a component as provider of a service would require an extra call.) Of course, we don’t want our component to be registered as a provider of the java.lang.Runnable
“service”. Therefore we have to enumerate the provided services (none) explicitly.
In order to make the annotations known to the java compiler, you have to add org.apache.felix.dependencymanager.annotation-x.y.z.jar
to the build path (as you did before with org.apache.felix.dependencymanager
, see above). In an additional build step, the annotations are then used to create a file META-INF/dependencymanager/io.github.mnl.osgiGettingStarted.simpleBundle.HelloWorld
in the bundle during packaging. Using the code above, the generated file looks like this:
{"impl":"io.github.mnl.osgiGettingStarted.simpleBundle.HelloWorld",
"stop":"stop",
"start":"start",
"type":"Component"}
{"service":"org.osgi.service.log.LogService",
"autoConfig":"logService",
"type":"ServiceDependency",
"required":"true"}
There are two ways to add the additional build step for creating this file to
the bnd
packaging tool. If you want to use the dependency manager in a
single project only, you can add the lines
-pluginpath:\
${workspace}/cnf/cache/org.apache.dependencymanager.annotation-5.0.0.jar;\
url=http://repo1.maven.org/maven2/org/apache/felix/org.apache.felix.dependencymanager.annotation/4.2.0/org.apache.felix.dependencymanager.annotation-5.0.0.jar
-plugin: org.apache.felix.dm.annotation.plugin.bnd.AnnotationPlugin;log=debug
to the project’s bnd.bnd
. If you want to enable support for the whole (bnd-)workspace,
add the lines to the build.bnd
in the cnf
project. The first line tells bnd
about the location of the plugin and the second instructs it to apply the plugin.
If you’re impatient and run the compiled bundle now, nothing’s going to happen.
As there is no activator any more, another mechanism must notice if a bundle
with files META-INF/dependencymanager/*
in it is started, read the information
from the files and act accordingly. Such a mechanism is provided by the dependency
manager as bundle org.apache.felix.dependencymanager.runtime-x.y.z.jar
.
Add it to the run bundles and start it again.
An important sentence in the article “Automating Service Dependency Management in a Service-Oriented Component Model” mentioned at the beginning is: The prototype platform for our research is implemented using the Open Services Gateway Initiative (OSGi) […] framework, but the ideas are general enough for use in other service platforms. This basically still holds true after the adoption of the concepts outlined in the article as an OSGi specification.
The OSGi alliance has added support for service components as “Declarative Services”, initially in Release 4 of the service specification (2005)5. “Declarative Services” as specified introduce a new layer of abstraction, which implies that you don’t need to know about some parts of the Core OSGi APIs any more in order to use OSGi as a service based component framework. Some OSGi enthusiasts therefore consider “Declarative Services” to be a kind of “fresh start” for OSGi and tell you to more or less forget about the service layer (API). I could follow this line of thought easily, if they had made “Declarative Services” the core of an “OSGi NG” and had dropped the no longer needed low level APIs6. As it is, I find that you still have to understand the basics in order to fully understand the (debug) outputs and solve some problems that you may encounter7.
As the name “Declarative Services” suggests, there is no API as in Felix DM.
Rather, services are declared using a header in MANIFEST.MF
, e.g.:
Service-Component: OSGI-INF/io.github.mnl.osgiGettingStarted.simpleBundle.HelloWorld.xml
The file name pattern “OSGI-INF/<fully qualified class name>.xml
” isn’t mandatory
but recommended.
If we want our component to be a declared service component, the file should look like this:
<?xml version="1.0" encoding="UTF-8"?>
<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0"
name="io.github.mnl.osgiGettingStarted.simpleBundle.HelloWorld"
activate="start" deactivate="stop">
<implementation class="io.github.mnl.osgiGettingStarted.simpleBundle.HelloWorld"/>
<reference name="LogService" interface="org.osgi.service.log.LogService"
bind="setLogService" unbind="unsetLogService"/>
</scr:component>
You would probably not be able to write this file from scratch without further
guidance, but when you read it, it’s quite self-explanatory. The only thing
that might be irritating is the reference to the log service, especially the
bind
and unbind
attributes. Well, some things have to be approached a bit
differently compared to Felix DM. Actually, I didn’t write the file shown above.
I used the annotations that have been added in release 4.3 of the specification
and let bnd
generate it8. The source code should clarify what happens
with the reference to the log service (the complete project is available
here).
package io.github.mnl.osgiGettingStarted.simpleBundle;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.log.LogService;
@Component(service={})
public class HelloWorld implements Runnable {
private static LogService logService;
private Thread runner;
@Reference
private void setLogService(LogService logService) {
this.logService = logService;
}
private void unsetLogService(LogService logService) {
this.logService = null;
}
@Activate
public void start(ComponentContext ctx) {
runner = new Thread(this);
runner.start();
}
@Deactivate
public void stop(ComponentContext ctx) {
runner.interrupt();
try {
runner.join();
} catch (InterruptedException e) {
logService.log(LogService.LOG_WARNING,
"Could not terminate thread properly", e);
}
}
@Override
public void run() {
System.out.println("Hello World!");
while (!runner.isInterrupted()) {
try {
logService.log(LogService.LOG_INFO, "Hello Word sleeping");
Thread.sleep (5000);
} catch (InterruptedException e) {
break;
}
}
}
}
The @Component
annotation for the class looks similar the one in the Felix DM
example. The parameter “service
” serves the same purpose as the parameter
“provides
” of the DM annotation: it prevents our component from being
registered as provider for service java.lang.Runnable
.
Declarative services supports setting the values of fields
starting with version 1.3 (OSGi version 6). Before this,
referenced services are made known to the component by invoking a setter method.
Considering our example, that’s not too bad because we can make the field with
the reference to the log service static again9. In order to avoid keeping
a reference to a log service implementation even if it disappears (and our
component is stopped), we have to provide an “unset” method as well10.
It doesn’t need an annotation. Rather, if there is a “setXYZ
” method with
the @Reference
annotation, declarative services automatically assume an
“unsetXYZ
” method to implement the corresponding “undo” operation.
As with Felix DM, you need a “Service Component Runtime” (as the OSGi
specification calls it)
to start up the service components. The SCR looks for Service-Component
headers
in all deployed bundles and creates service components, registers their
services and activates the components according to the directives in the XML
file. Of course, you can use any implementation. But in our environment,
the easiest way is to add the bundle org.apache.felix.scr
to the run bundles.
Since Felix SCR has a dependency on an OSGi service called “Configuration Admin”,
we actually have to add two bundles to the configuration, which results
in a bnd.bnd
that looks like this:
-buildpath: \
osgi.cmpn;version=4.3.1,\
osgi.core;version=4.3.1,\
osgi.annotation
-runbundles: \
org.apache.felix.log,\
org.apache.felix.gogo.command,\
org.apache.felix.gogo.runtime,\
org.apache.felix.gogo.shell,\
de.mnl.osgi.log.fwd2jul,\
org.apache.felix.configadmin,\
org.apache.felix.scr
Compared to using the OSGi Core API, Declarative Services makes providing
a service a lot easier. A provider for the calculator service from chapter
Providing a Service can simply be made available
by annotating the implementation class as Component
. We don’t need an activator
any more (you can find the complete project
here).
package io.github.mnl.osgiGettingStarted.calculator.ds;
import org.osgi.service.component.annotations.Component;
import io.github.mnl.osgiGettingStarted.calculator.Calculator;
@Component(property = Constants.SERVICE_VENDOR + "=Michael N. Lipp")
public class CalculatorImpl implements Calculator {
@Override
public double add(double a, double b) {
return a + b;
}
}
The Component
annotation has a lot of optional elements that can be used to further specify
the properties of the service that is provided.
A property
element can be used to specify properties that
are added when the service is registered (as explained in
“Providing a Service”).
Similar to registering services using the OSGi core API, you can specify a scope
for the service provided by a component.
The default scope is SINGLETON
(with BUNDLE
or PROTOTYPE
as alternatives).
By default, the SCR delays the activation of a service (and thus the creation of the service component) until the service is requested. This behavior can speed up the startup time considerably.
If you want a service component to be created and activated immediately on
startup, you have to set the immediate
element of the component
annotation to true
.
Usually, a single component instance is created (and any provided service is registered) when all referenced services have become available. Sometimes, however, you may want to create one or more instances of a component (and register its provided services) dynamically at runtime.
This is made possible by specifying a factory
attribute with an identifier.
If defined, SCR registers a (factory) service with this identifier that
provides a single method newInstance
. This service can then be used to
dynamically create component instances.
Note that this approach is different from creating components using
a ManagedServiceFactory
as presented in
“Configuration Admin”. More on the
differences will be explained in the next section.
The SCR is closely coupled with Configuration Admin. Every component has a configuration PID. If not specified explicitly, it defaults to the component’s name which in turn defaults to the name of the implementation class.
By default, values stored in Configuration Admin are merged with the values specified in the component annotation, with the former taking precedence over the latter. To demonstrate this behavior, run the project that implements the calculator service using Declarative Services and execute the following commands in the GoGo shell (output partially shortened or omitted):
g! serviceReference io.github.mnl.osgiGettingStarted.calculator.Calculator
Properties [service.vendor=Michael N. Lipp, component.id=0, component.name=io.github.mnl.osgiGettingStarted.calculator.ds.CalculatorImpl, ...
g! conf = cm:getConfiguration io.github.mnl.osgiGettingStarted.calculator.ds.CalculatorImpl "?"
g! $conf update (new java.util.Hashtable ["service.vendor"="Nobody"])
g! serviceReference io.github.mnl.osgiGettingStarted.calculator.Calculator
Properties [service.vendor=Nobody, ...]
...
If the log level is set to “information”, you can observe that updating the properties in Configuration Admin causes SCR to unregister the service and re-register it. This means that changing properties this way may be a costly operation.
How the information from Configuration Admin is handled by SCR
can be configured for each component individually by specifying
a configurationPolicy
element. The default value is OPTIONAL
which
results in the behavior demonstrated above.
An interesting alternative is the value REQUIRE
. Add
configurationPolicy = ConfigurationPolicy.REQUIRE
to the parameters of
the @Component
annotation and restart the framework. Try to find our
calculator service in the GoGo shell.
g! serviceReference io.github.mnl.osgiGettingStarted.calculator.Calculator
It’s no longer there. REQUIRED
means that SCR won’t create an
instance of the component unless there is a configuration with the
component’s PID. Try:
g! conf = cm:getConfiguration io.github.mnl.osgiGettingStarted.calculator.ds.CalculatorImpl "?"
g! $conf update null
g! serviceReference io.github.mnl.osgiGettingStarted.calculator.Calculator
Properties [service.vendor=Michael N. Lipp, component.id=0, ...]
...
… and the service is back again. Effectively this means that a component won’t be created with its default properties. It will only be created when it has been configured explicitly. Of course, if you make sure that the configuration isn’t deleted, the configuration and thus the service will be immediately available after a restart.
Maybe one of the most curious characteristics of this policy is that the component’s PID can – without any further change – also be used as a factory PID in Configuration Admin. Let’s create two instances of the calculator:
g! conf = cm:getFactoryConfiguration io.github.mnl.osgiGettingStarted.calculator.ds.CalculatorImpl Test1 "?"
g! $conf update null
g! conf = cm:getFactoryConfiguration io.github.mnl.osgiGettingStarted.calculator.ds.CalculatorImpl Test2 "?"
g! $conf update null
g! getAllServiceReferences io.github.mnl.osgiGettingStarted.calculator.Calculator null
000032 12 Calculator io.github.mnl.osgiGettingStarted.calculator.ds.CalculatorImpl~Test2
000031 12 Calculator io.github.mnl.osgiGettingStarted.calculator.ds.CalculatorImpl~Test1
Of course, a real use case will pass different sets of properties when
calling the update
method, thus configuring different instances of
a service (e. g. two instances of a network client each connecting
to a different server).
There are thus two types of dynamically created components. Components
created by invoking the newInstance
method of a factory service provided
by SCR for a component factory (as described in the previous section),
and components created by providing a factory configuration.
The former are transient, the latter persist across restarts11.
Note that this is the point where the term “component”
is formally introduced in OSGi. I have sometimes referred to the
HelloWorld
class as a component, but up to now, this has been
my conceptual view on this class, nothing formalized. ↩
Unfortunately, stop
is final and cannot be overridden. I’ll improve the
solution in the second part. ↩
It can be outright dangerous. Imagine that the calculator
service from “Providing a service” is missing and all invocations
of add
simply return 0 as result. ↩
The “required
” element included in the @ServiceDependency
cannot be found in the “manual” pages, which obviously aren’t complete. When in doubt, have a look at the javadoc. ↩
I recommend to first have a look at the initial version of the specification. It is easier to read because it focuses on the core functions. And it uses event based explanations of the internal workings, which is close to the specification of the service layer. Starting with version 7, the specifications use the more abstract concept of dependency injection. ↩
And IMHO if they had also switched to an easier to read style for the specifications, this might have been a successful new beginning for OSGi as well. ↩
Not knowing about the underlying mechanisms is a bit like teaching programmers nothing about memory management because “Java does it automatically”. And then somebody (a few days before the deadline) says: “Does anybody know what this OutOfMemory exception means that I keep getting from time to time?” ↩
With declared services, you only have to add osgi.annotation
to the
build path to make bnd
generate this file. Handling the OSGi annotations is
built-in and doesn’t require the configuration of a plugin. ↩
You can make Felix DM invoke a method, too. ↩
You don’t need an “unset” method for ordinary (non-static) attributes because the instance of the service component itself is discarded when a component is deactivated. ↩
Neil Bartlett wrote on stackoverflow:
You almost certainly don’t want to use ComponentFactory, so I recommend ignoring it.
I’m not sure if this comment is correct, I can imagine use cases for both. ↩