Dependency Manager - Getting Started
When developing an OSGi bundle that has dependencies and possibly registers services, there are two classes in particular we need to implement:
- The bundle activator which controls the life-cycle of the bundle.
- The actual component, which can be a POJO.
When using the dependency manager, your bundle activator is a subclass of DependencyActivatorBase
. It needs to implement the init
life cycle method and can optionally also implement a destroy
method. Both methods take two arguments: BundleContext
and DependencyManager
. The latter is your interface to the declarative API you can use to define your components and dependencies.
The following paragraphs will show various examples that explain how to do this. Subsequently, some more advanced scenarios will be covered that involve listening to dependency and component state changes and interacting with the OSGi framework from within your component implementation.
To use the dependency manager, you should put the org.apache.felix.dependencymanager.jar
in your classpath while compiling and in your OSGi framework when running.
A recent java 8 jdk is required since the dependency manager r8 (the dm r8 release has been built and tested using java version 1.8.74).
Registering a service¶
The first example is about registering a service. We extend DependencyActivatorBase
and in the init
method we use the reference to the DependencyManager
to create and add a component. For this component we subsequently set its service interface and implementation. In this case the interface is the Store
interface, the second parameter, null
, allows you to provide properties along with the service registration. For the implementation, we only mention the Class
of the implementation, which means the dependency manager will lazily instantiate it. In this case, there is not much point in doing that because the component has no dependencies, but if it had, the instantiation would only happen when those dependencies were resolved.
Notice that the dependency manager API uses method chaining to create a more or less "fluent" API that, with proper indentation, is very easy to read.
public class Activator extends DependencyActivatorBase { public void init(BundleContext context, DependencyManager manager) throws Exception { manager.add(createComponent() .setInterface(Store.class, null) .setImplementation(MemoryStore.class) ); } }
This is the service interface. Nothing special here.
public interface Store { public void put(String key, Object value); public Object get(String key); }
And finally the implementation. Again, this is just a POJO, there is no reference here to any OSGi or dependency manager specific class or annotation.
public class MemoryStore implements Store { private Map m_map = new HashMap(); public Object get(String key) { return m_map.get(key); } public void put(String key, Object value) { m_map.put(key, value); } }
Depending on a service¶
Our second example is that of a component that depends on two other services: our Store
from the previous example and the standard OSGi LogService
. Looking at the code, there is a small but important difference between the two: Store
is a required dependency and LogService
is not. This means that our component really needs a store to work, but if there is no logging available, it can work without. Also note that this component has no setInterface
method, which simply means it is not itself a service. This is perfectly fine.
public class Activator extends DependencyActivatorBase { public void init(BundleContext context, DependencyManager manager) throws Exception { manager.add(createComponent() .setImplementation(DataGenerator.class) .add(createServiceDependency() .setService(Store.class) .setRequired(true) ) .add(createServiceDependency() .setService(LogService.class) .setRequired(false) ) ); } }
Now let's look at our POJO. There are a couple of interesting things to explain. First of all, our dependencies are declared as fields, and they don't even have setters (or getters). When the dependency manager instantiates our class, it will (through reflection) inject the dependencies so they are just available for our class to use. That is also the reason these fields are declared as volatile: to make sure they are visible to all threads traversing our instance.
One final note, since we defined our LogService
dependency as optional, it might not be available when we invoke it. Still, the code does not contain any checks to avoid a null pointer exception. It does not need to, since the dependency manager makes sure to inject a null object when the real service is not available. The null object can be invoked and will do nothing. For a lot of cases that is good enough, but for those cases where it is not, our next example introduces callbacks that notify you of changes.
public class DataGenerator { private volatile Store m_store; private volatile LogService m_log; public void generate() { for (int i = 0; i < 10; i++) { m_store.put("#" + i, "value_" + i); } m_log.log(LogService.LOG_INFO, "Data generated."); } }
Tracking services with callbacks¶
Sometimes, simply injecting services does not give you enough control over a dependency because you might want to track more than one, or you might want to execute some code on changes. For all those cases, callbacks are your friends. Since one of our goals is to not introduce any kind of API in our POJO, callbacks are declared by specifying their method names instead of through some interface. In this case, we have a dependency on Translator
services, and we define added
and removed
as callbacks.
public class Activator extends DependencyActivatorBase { public void init(BundleContext context, DependencyManager manager) throws Exception { manager.add(createComponent() .setImplementation(DocumentTranslator.class) .add(createServiceDependency() .setService(Translator.class) .setRequired(false) .setCallbacks("added", "removed") ) ); } }
This is the actual Translator
service, which, for the purpose of this example, is not that important.
public interface Translator { public boolean canTranslate(String from, String to); public Document translate(Document document, String from, String to); }
Finally, here's our implementation. It declares the callback methods with one parameter: the Translator
service. Actually, the dependency manager will look for several different signatures (all explained in more detail in the reference section).
public class DocumentTranslator { private List<Translator> m_translators = new ArrayList<Translator>(); public void added(Translator translator) { m_translators.add(translator); } public void removed(Translator translator) { m_translators.remove(translator); } public Document translate(Document document, String from, String to) { for (Translator translator : m_translators) { if (translator.canTranslate(from, to)) { return translator.translate(document, from, to); } } return null; } }
Depending on a configuration¶
Not all dependencies are on services. There are several other types of dependencies that are supported, one of them is the configuration dependency. When defining the dependency, you must define the persistent ID of the service. The component will not become active until the configuration you depend on is available and is valid. The latter can be checked by your implementation as we will see below.
public class Activator extends DependencyActivatorBase { public void init(BundleContext context, DependencyManager manager) throws Exception { manager.add(createComponent() .setImplementation(Task.class) .add(createConfigurationDependency() .setPid("config.pid") ) ); } }
Here's our code that implements ManagedService
and has an updated
method.
This method checks if the provided configuration is valid and throw a
ConfigurationException
if it is not. As long as this method does not accept the
configuration, the corresponding component will not be activated.
Notice that your component does not necessarily implement the ManagedService interface, and the updated callback
can also throw any exceptions:
public class Task implements ManagedService { private String m_interval; public void execute() { System.out.println("Scheduling task with interval " + m_interval); } public void updated(Dictionary properties) throws ConfigurationException { if (properties != null) { m_interval = (String) properties.get("interval"); if (m_interval == null) { throw new ConfigurationException("interval", "must be specified"); } } } }