Usage

Let's look at how to use this implementation. When developing an OSGi bundle that uses and possibly registers services, there are two classes in particular we need to implement. The first one is the bundle activator which controls the life-cycle of the bundle. The second one is the actual service implementation.

When using the dependency manager, your bundle activator is a subclass of DependencyActivatorBase. The following paragraphs will explain how to specify a service and its dependencies. Subsequently, some more advanced scenarios will be covered that involve listening to dependency and service state changes and interacting with the OSGi framework from within your service implementation.

Registering a service

Registering a service is done using a custom implementation of the bundle activator, DependencyActivatorBase, which requires you to implement two methods: init and destroy.

Each bundle can specify zero or more services in the init method. Services can optionally have an interface and properties with which they're registered into the service registry. Registering a service involves creating a new service and adding it to the dependency manager. You then specify the interface as well as the implementation, as shown in the example.

public HttpActivator extends DependencyActivatorBase {
  public void init(BundleContext ctx, DependencyManager manager)
                                  throws Exception {
    Properties props = new Properties();
    props.put("port", "8080");
    manager.add(createService()
      .setInterface(WebService.class.getName(), props)
      .setImplementation(WebServiceImpl.class));
  }
  public void destroy(BundleContext ctx, DependencyManager manager)
                                  throws Exception {
    // cleaned up automatically
  }
}

There are a couple of alternatives that need some clarification. First of all, you might not have a public interface at all, so the setInterface call is optional. Like in OSGi itself, you can also specify multiple service interfaces in an array of strings. For the implementation you have two choices. You can either specify the class of the implementation, in which case this class needs to have a default, no-args constructor and the dependency manager will use lazy instantiation or you can specify an instance you've already created. More on the life-cycle of the service implementation can be found in the next paragraph.

Specifying service dependencies

Dependencies can either be required or optional. Required dependencies need to be resolved before the service can even become active. Optional dependencies can appear and disappear while the service is active. The example demonstrates how both are specified in the activator.

    manager.add(createService()
      .setInterface(WebService.class.getName(), null)
      .setImplementation(WebServiceImpl.class)
      .add(createServiceDependency()
        .setService(ConfigurationAdmin.class, null)
        .setRequired(true))
      .add(createServiceDependency()
        .setService(LogService.class, null)
        .setRequired(false))

Specifying configuration dependencies

Configuration dependencies specified here are by definition required. The reason for this is that if you need an optional configuration, simply registering as a ManagedService(Factory) and implementing updated(Dictionary) works fine. For required configuration dependencies, you can use code along these lines:

    manager.add(createService()
      .setInterface(HttpServlet.class.getName(), null)
      .setImplementation(MyServlet.class)
      .add(createConfigurationDependency()
        .setPid("org.apache.felix.myservlet")
        .setPropagate(true))

As you can see, you do not need to register as ManagedService yourself. You do need to implement ManagedService in your implementation though. When implementing the updated() method, take the following things into account:

  1. updated() will be called after you are instantiated, but before init() and start() are invoked, or service dependencies are injected, so make sure this method works like this;
  2. if updated() executes normally, this dependency will become resolved;
  3. if updated() throws a ConfigurationException, you are signalling the configuration is invalid and the dependency won't be resolved (or it will become unresolved if it previously was);
  4. if updated() gets a null configuration, the dependency will become unresolved too;

In some cases, you might want to propagate configuration settings to your service properties. In that case, use the setPropagate(true) method to enable automatic property propagation (the default is false). That means any properties you specify yourself and any properties you get from ConfigurationAdmin will be merged, and your service will automatically be updated every time these properties change.

Implementing the service

The service implementation is pretty straightforward. Basically any class that implements the service interface can be used here. Two aspects of the implementation deserve some attention though: the life-cycle model and the service dependencies.

Depending on how the service was specified in the activator it is either instantiated as soon as all required dependencies become available or it was already instantiated when the activator started. The service then goes through a number of states, where each transition includes the invocation of a method on the service implementation. The default names of these methods are init, start, stop and destroy but these can be altered. As a developer, all you need to do is specify any of these methods in your service implementation and they will be invoked.

public class WebServiceImpl implements WebService {
  public void init() {
  }
  public void start() {
  }
  public void stop() {
  }
  public void destroy() {
  }
}

Upon activation, the service implementation should initialize its internal state in the init() method and establish connections with other services in the start() method.

The dependency manager will automatically fill in any references to required dependencies that are specified as attributes. The same goes for optional dependencies if they are available. If not, those will be implemented by a null object (warning) TODO-ref. In short, this allows you to simply use these interfaces as if they were always available. A good example of this is the LogService. If it's available, we want to use it for logging. If not, we want to simply ignore log messages. Normally, you'd need to check a reference to this service for null before you can use it. By using a null object, this is not necessary anymore.

public class WebServiceImpl implements WebService {
  private volatile ConfigurationAdmin configAdminSvc;
  private volatile LogService logSvc;
  
  public void loadSettings() {
    logSvc.log(LogService.LOG_INFO, "Loading settings.");
    // do stuff here
  }
}

Using an instance factory

(warning) TODO will explain how to provide a factory class and method that will be used to create the service implementation.

Using compositions

Sometimes, a service implementation consists of more than a single instance. Instead, it can be a composition of a number of instances. That might also mean that you need dependencies injected into more than one instance. If that is the case, you can provide a list of instances to use for dependency injection yourself.

To do that, you can specify the name of a method that will return a list of instances, like this:

    manager.add(createService()
      .setImplementation(MainServiceImpl.class)
      .setComposition("getInstances"));

Whenever dependencies need to be injected, the dependency manager will query the method and try to inject dependencies into all the instances this method returns (as an Object[]). For example:

public class MainServiceImpl {
    private SubComponentA m_compA;
    private SubComponentB m_compB;

    public MainServiceImpl() {
        m_compA = new SubComponentA();
        m_compB = new SubComponentB();
    }

    public Object[] getInstances() {
        return new Object[] { this, m_compA, m_compB };
    }
}

Listening to service dependencies

Optionally, a service can define callbacks for each dependency. These callbacks are invoked whenever a new dependency is discovered, an existing dependency changes or disappears. They allow you to track these dependencies. This can be very useful if you, for example, want to implement the white board pattern.

    manager.add(createService()
      .setImplementation(DeviceListener.class)
      .add(createServiceDependency()
        .setService(Device.class, null)
        .setCallbacks("added", "changed", "removed")
        .setRequired(false));
public class DeviceListener {
  public void added(ServiceReferende ref, Object service) {
  }
  public void changed(ServiceReference ref, Object service) {
  }
  public void removed(ServiceReference ref, Object service) {
  }
}

There are actually several signatures you can use for the callback methods. The dependency manager will search for them in this order (we use a dependency on Device as an example):

  1. ServiceReference, Device
  2. ServiceReference, Object
  3. ServiceReference
  4. Device
  5. Object
  6. (void)

The search ends when the first signature in this list is found. That method will actually be invoked.

Listening to the service state

If you're interested in the state of a service, you can register a service state listener with the service of interest. It will be notified whenever the service is starting, started, stopping and stopped.

    Service s = createService()
      .setInterface(WebService.class.getName(), null)
      .setImplementation(WebServiceImpl.class);
    s.setStateListener(new ServiceStateListener() {
        public void starting(Service s) {
        }
        public void started(Service s) {
        }
        public void stopping(Service s) {
        }
        public void stopped(Service s) {
        }
      });
    manager.add(s);

Interacting with OSGi

Although interaction of the service implementation with the OSGi framework has nothing to do with dependency managemement, the dependency manager does offer support for communicating with the framework.

Normally, your service implementation does not have to contain any OSGi specific classes or code. As shown before, even references to other services can be used without dealing with OSGi specifics. Sometimes, this is an advantage, since the service implementation can easily be reused in a different service oriented framework. However, there are times when you do want to access, for example, the BundleContext because you want to interact with the framework.

For these cases, you can specify one or two members to get direct access to:

  1. the BundleContext that provides you with an interface to the OSGi framework;
  2. the ServiceRegistration of the service that was registered, provided you have registered a public service interface.
public class WebServiceImpl implements WebService {
  private BundleContext ctx;
  private ServiceRegistration svcReg;

  //  
}

Dynamic dependencies

Most of the time, the dependency definitions are static. You define them when the bundle starts and they stay the same throughout the life-cycle of the bundle. However, in some cases you might want to add dependencies to a service afterwards. For example, you might want to start tracking a service with specific properties. To do this, simply add the dependency in the same way as you would have done in the init() method.

  • No labels