Getting Started

When developing an OSGi bundle that has dependencies and possibly registers services, there are two classes in particular we need to implement:

  1. The bundle activator which controls the life-cycle of the bundle.
  2. The actual component implementation, which can be a POJO.

When using the dependency manager, your bundle activator is a subclass of DependencyActivatorBase. It needs to implement two life cycle methods: init and destroy. 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.

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.getName(), null)
            .setImplementation(MemoryStore.class)
        );
    }
    
    public void destroy(BundleContext context, DependencyManager manager) throws Exception {}
}

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)
            )
        );
    }
    
    public void destroy(BundleContext context, DependencyManager manager) throws Exception {}
}

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")
            )
        );
    }
    
    public void destroy(BundleContext context, DependencyManager manager) throws Exception {}
}

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. In fact, only required configuration dependencies are supported, because optional ones can just be achieved by registering as a ManagedService yourself. 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")
            )
        );
    }
    
    public void destroy(BundleContext context, DependencyManager manager) throws Exception {}
}

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.

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");
            }
        }
    }
}

...

  • No labels