Getting Started with OSGi Declarative Services (2024 Edition)

44 minute read

In 2016 I wrote the first Getting Started with OSGi Declarative Services blog post and several additional blog posts about OSGi DS. It was written for developers or Eclipse RCP applications at a time when OSGi DS was not well known to them. Also the tooling support in PDE was not that good, and the usage of the annotation based programming model for OSGi services was quite new. Since then a lot happened in that area, and there are updates in the OSGi specification and the tooling. Therefore I decided to create an updated version of the initial blog post(s). Most of the information is the same, as the basics in OSGi services haven’t changed. But the exercises are updated to the latest Eclipse IDE and it contains information about the OSGi Declarative Services Specification v1.5.

In this tutorial you will find an introduction to OSGi declarative services. What a component is and how it is created using DS annotations.

Note:
I will use the PDE project layout with Automatic Manifest Generation that was introduced as part of PDE with Eclipse 4.28. If you are interested in using OSGi DS with the classical PDE Plug-in project layout, please have a look at the old blog post. Although even there several informations are outdated, e.g. the optional package import of org.osgi.service.component.annotations should not be necessary anymore, and the launch configuration is slightly different because of changes in the bundle structure.

You can expect:

  • General information about OSGi Declarative Services
  • The usage of Declarative Services Annotations
  • A very basic comparison between PDE and Bndtools
  • An introduction to the Declarative Services Specification 1.5

The tutorial is similar to the Bndtools tutorial. Therefore we basically create three bundles:

  • Service API
  • Service Provider
  • Service Consumer (a Felix Shell command)

If you are interested in using Bndtools and not PDE, you can read on for the general information and then move over to the Bndtools tutorial. It is quite good to get started with Bndtools and Declarative Services. For the curious I will try to point out the differences between PDE and Bndtools. But only with regards to this tutorial. For sure Bndtools serves a lot more features, but I don’t want to focus on that topic and just give an idea on the differences to start with.

1. Introduction

OSGi has a layered model to provide several concepts. Mostly the Bundles and Modules are seen when talking about OSGi, where the focus is on modularization. But the Services layer is at least as interesting as the Bundles and Modules. Regarding OSGi services you will find the following (or similar) picture in various documentations and tutorials .

Without repeating the already existing documentation, it means that a bundle A can provide/publish a service implementation S and another bundle B can consume it. This is done by finding a service and binding to it when it is found. As there can be multiple service implementations published at the same time for the same type, and services in OSGi are dynamic and can come and go at runtime, the service consumer is listening to be able to react accordingly. This contract is implemented by the OSGi ServiceRegistry.

Compared with the Equinox only extension points, that are widely used in Eclipse 3.x based RCP applications, there are several advantages when using OSGi declarative services. First is the fact that multiple bundles can provide services and multiple bundles can consume them, which means there is a many-to-many relationship. In Equinox one bundle specifies an extension point and multiple bundles can contribute extensions to it, which makes it technically a one-to-many relationship. Of course you can also access the extension points of another bundle via the ExtensionRegistry, but conceptually this feels like a workaround compared with the general publish-find-bind mechanism of OSGi services. The second fact is that you access extension points via ID, while OSGi services are retrieved by type. That means OSGi service retrieval is type-safe, while extension points are not. Further information about the comparison between extension points and OSGi services can be found here: A Comparison of Eclipse Extensions and OSGi Services

With declarative services it is not necessary to register or consume services programmatically. This needs to be done with plain OSGi services where a service is typically registered (publish) to the ServiceRegistry in an Activator and consumed (find-bind) via ServiceTracker (also mostly via Activator). Instead of this a Service Component is declared via Component Description when using declarative services. The Component Description is an XML file that is processed by a Service Component Runtime (SCR, e.g. Felix SCR) when a bundle is activated. It is responsible for managing the components and their life cycle. That means, if you want to use declarative services in your application, you need to ensure that a Service Component Runtime bundle is installed and activated in your environment.

1.1 Components

When talking about OSGi declarative services you will always talk about components. The following list will give an overview of the necessary wordings related to components to distinguish the different parts:

  1. Service Component
    A Java class inside a bundle that is declared via Component Description and managed by a Service Component Runtime. Note that although we are talking about Declarative Services and Service Components, not every component provides a service!
  2. Component Description
    The declaration of a Service Component, contained in an XML document.
  3. Component Configuration
    A Component Description that is parameterized with component properties. It is used to track the component dependencies and manages the Component Instance.
  4. Component Instance
    The instance of the component implementation class. It is created when a Component Configuration is activated and discarded if the Component Configuration is deactivated.

As nobody wants to write XML files manually nowadays and the Component Definition Editor in PDE is the worst Eclipse editor ever (which you nowadays will probably not even find anymore), it is recommended to use the Declarative Service Annotations to get the Component Definition generated. I will therefore also not look at the generated XML document. If you are interested in that, have a look at the specification.

1.2 References to Services

Components can have dependencies on other components. If these dependencies should be managed automatically by the SCR, this is restricted to components that provide a service. Otherwise the SCR is not able to resolve the dependency.

There are different terms used when reading about service references. These terms are mostly related to the state they are in:

  • Reference
    The definition of a dependency to another service.
  • Target Services
    The services that match the reference interface and target property filter. They are needed to satisfy a Component Configuration.
  • Bound Services
    The services that are bound to a Component Configuration. The binding is done on activating a Component Configuration.

There are different ways to access a Bound Service:

  • Method Injection
    With this strategy the SCR calls Event Methods when a service becomes bound, unbound or its configuration properties are updated.
  • Field Injection (since DS 1.3)
    SCR modifies a field in the component instance when a service becomes bound, unbound or its configuration properties are updated.
  • Constructor Injection (since DS 1.4) When SCR activates a component instance, the component instance must be constructed and constructor injection occurs. Bound services and activation objects can be parameters to the constructor.
  • Lookup Strategy
    The bound service is located programmatically via ComponentContext.

I will add more detailed information on references at the end of this tutorial, with the description of the @Reference annotation.

1.3 Lifecycle

An important information regarding Service Components is that they have their own lifecycle, which is contained in the life cycle of a bundle. The most important question regarding Service Components is, when is it available for usage. To answer this question it is also important to know that there are basically three types of components with regards to the component life cycle:

  • Delayed Component
    Activation is not done until there is a request for a service object. Therefore even class loading and instantiation can be delayed until that time. A Delayed Component needs to specify a service.
  • Immediate Component
    Activated as soon as its dependencies are satisfied. An Immediate Component does not need to specify a service.
  • Factory Component
    Creates and activates new Component Configurations on request. The created Component Configurations are not re-used if they become unsatisfied or unregistered.

Typically you find information about Delayed Components when reading about OSGi Declarative Services.

The following states are possible:

  • Enabled/Disabled
    The initial enabled state of a component is specified via Component Description. All components are disabled when the bundle is stopped. It is possible to change the enabled state programmatically at runtime via CompontentContext.
  • UNSATISFIED
    The component is not ready to be started. See Satisfied for the necessary criteria. This status can also be reached again if a component is not Satisfied anymore.
  • Satisfied
    A component is in a Satisfied state when it is enabled and the required referenced services are available. In case the ConfigurationAdmin is used and the configuration-policy=required is specified, also a configuration object needs to be available to bring a component to satisfied state.
  • REGISTERED
    A component is in REGISTERED state if it Satisfied and not yet requested. Only applies for Delayed Components.
  • ACTIVE
    The component was activated due to immediate activation or, in case of a Delayed Component, it was requested.

The following images show the lifecycle of an Immediate Component and a Delayed Component. As I will not write about Factory Components here, I leave them out and come up with it in another post.

Immediate Component Lifecycle

Delayed Component Lifecycle

When a bundle is started, the SCR is checking if the bundle contains Component Descriptions. This is done via Extender Pattern, which means it searches for a Service-Component entry in the bundle MANIFEST header. If it finds one it will start to process the Component Description and create a Component Configuration. One of the first checks is the initial enabled state. Only if the Component Configuration is enabled, the SCR will try to satisfy the dependencies by finding and binding the specified references. It will also try to satisfy the configuration, if that is required by the Component Description. That means it checks for required references and configurations if necessary. After the Component Configuration is satisfied, it can be activated. An Immediate Component will activate immediately, a Delayed Component moves to the REGISTERED state, awaiting the first request to the provided service. If a Component Configuration contains dynamic references, the references can rebind in ACTIVE state, otherwise it will be re-activated. If a Component Configuration becomes unsatisfied (e.g. a bound service becomes unavailable), the Component Configuration will be deactivated. Note that a Delayed Component will also be deactivated and gets back to REGISTERED state in case no other bundle references it anymore.

The activation of a component and the time when it is done makes the real difference between an Immediate and a Delayed Component. It consists of the following steps:

  1. Load the component implementation class
  2. Create the component instance and component context
  3. Bind the target services
  4. Call the activate method if present

For Delayed Components the initial memory footprint and the load time is therefore delayed on startup until the first request on a service object (see Declarative Services Specification – 112.5.6 Activation).

In the above diagrams the light-blue highlighted states and state transitions indicate that a Component Instance exists. Correlated to the explanation on the activation of a component, this should make it more clear where a Component Instance (and therefore a real object) comes to play.

This is also a big difference to Eclipse Extension Points. While with OSGi Declarative Services and Delayed Components a bundle can be safely activated without issues regarding startup performance and initial memory footprint, the policy in Equinox and Eclipse is to optimize the startup on bundle level. That means you can use Extension Points and Core Expressions to avoid starting a bundle until the first usage of an extension.

Now enough with the general basics and let’s get started with the tutorial! I will place some further general information (e.g. describing the DS annotations) alongside the matching places in the tutorial and at the end.

2. IDE Setup

If you are using Eclipse >= 4.28 (2023-06) and try the Automatic Manifest Generation, there is no need for an additional configuration.

If you use an older version of Eclipse that does not yet support the Automatic Manifest Generation or you want to stick with the olde PDE plugin project layout, you need to enable the DS Annotations support. To do this open the preferences via Window -> Preferences -> Plug-in Development -> DS Annotations and check Generate descriptors from annotated sources.

After that the following four configurations are available:

  • Descriptor directory
    The directory to which the component description files will be generated. Default is OSGI-INF, and you should leave that setting unchanged.
  • Specification version
    The version of the OSGi Declarative Services Specification that should be used.
  • Annotation problem level
    If issues on annotation level should be reported as an Error, Warning or to Ignore them. Default is Error and it is a good idea to keep that to see if the annotation is used correctly.
  • Missing implicit reference unbind method
    If a missing unbind method for a service reference should be reported as an Error, Warning or to Ignore them. The default is Error. The DS specification does not require an unbind method, but it is strongly suggested in case of dynamic references that are stored locally.
  • Generate header “Bundle-ActivationPolicy: lazy”
    If this setting is enabled, the bundle manifest header will be generated. The default is enabled, and it should be enabled in case Equinox is used as OSGi framework.

Bndtools vs. PDE

The difference to Bndtools, well you need to install Bndtools into your Eclipse installation. And there is no need to configure the annotation processing separately. This is also not necessary with the Automatic Manifest Generation in PDE, which makes it much easier to use DS with PDE nowadays.

Interlude: Bundle-ActivationPolicy: lazy

The Lazy Activation Policy that is configured via Bundle-ActivationPolicy, is a bundle life cycle policy that tells the OSGi framework that a bundle should be lazily activated when the first successful class load is made from its local class space. From my research I’ve found out that this policy is typically not used when working with other OSGi frameworks. Apache Felix or Eclipse Concierge for example can be configured to automatically install and start all bundles in a specified directory. Also Equinox can be configured to automatically install and start all bundles, but that needs to be explicitly configured for every bundle itself. The default Eclipse Configurator, currently SimpleConfigurator and the deprecated update configurator, only install all bundles in the plugins directory but doesn’t start them. By specifying the Lazy Activation Policy you basically specify an auto-start behavior for bundles without the need to specify the auto-start manually in a launch configuration. As you typically don’t specify an auto-start for every bundle that provides service implementations via DS, the lazy activation policy is mandatory in Eclipse with Equinox to be sure that the service implementation is available. Otherwise the bundle that provides the service might never be started. The main idea behind not automatically starting all bundles was to reduce the startup time by reducing the number of bundles to activate. From my understanding the startup performance issues in the past (when that policy was added) where related to ineffectively implemented bundle activators. If you follow the best practices in OSGi development and use declarative services, you shouldn’t use Activators at all. The bundle startup should take almost no time and the component activation can be delayed to the first request (see lifecycle above). There should be only rare cases where you can’t use Service Components and an Activator is really needed.

3. API Project

Let’s start with the tutorial by specifying the service API. This is typically done in a separate API project to decouple the service contract and the service implementation, which makes it possible to exchange or provide multiple implementations for a service.

I recently heard the remark that it is “old-school” to always create an interface if there is only one implementation. But regarding a service-oriented-design you should always consider using interfaces and even separate the interfaces in an API bundle. Even if you only see one implementation, consider the following two statements:

  1. Don’t forget testing!
    By separating API and implementation you can simply create a test implementation of a service that is provided by a separate bundle. Especially for UI testing you can deploy the bundle with the test implementation of a service instead of deploying the real service implementation that needs to be mocked in test execution.
  2. Clean dependency hierarchies
    An API should typically have no or at least little dependencies to other libraries. Only implementations should have such dependencies. If the API without additional third-party-depencies is separated in an API bundle, and a service consumer only depends on the API, the consumer also has a simpler dependency graph.

Regarding Service Components that provide a service it is therefore always recommended to have the API in a separate bundle. For sure there are also exceptions to that rule, e.g. Immediate Components that are used for initial configuration or used to open a socket for communication.

3.1 Create an API project

In the Plug-in Development Perspective

  • Create a new plug-in project
    • Main Menu → File → New → Plug-in Project
    • Set name to org.fipro.inverter.api
    • In the Target Platform section select
      • This plug-in is targeted to run with: an OSGi framework:
      • Select standard in the combobox
      • Check Generate OSGi metadata automatically
    • Click Next
    • Set Name to Inverter Service API
    • Select Execution Environment JavaSE-17
    • Ensure that Generate an Activator and This plug-in will make contributions to the UI are disabled
    • Click Finish
    • If you do not see the tabs at the bottom of the recently opened editor with name org.fipro.inverter.api, close the editor and open the pde.bnd file in the project org.fipro.inverter.api.
      • Switch to the pde.bnd tab
        • Add the -runee instruction to create the requirement on Java 17
          Bundle-Name: Inverter Service API
          Bundle-SymbolicName: org.fipro.inverter.api
          Bundle-Vendor: 
          Bundle-Version: 1.0.0.qualifier
          -runee: JavaSE-17
          

Note:
You can add additional headers like Automatic-Module-Name: org.fipro.inverter.api and instructions like -runee: JavaSE-17 to the pde.bnd file. This has effect on the generation of the MANIFEST.MF file. For this example they are not necessary. The list of OSGi manifest headers can be found in bnd - Headers and the list of possible instructions is available in the bnd - Instruction Index.

3.2 Specify the API

  • Create an interface for the service definition
    • Main Menu → File → New → Interface
      • Source Folder: org.fipro.inverter.api/src
      • Package: org.fipro.inverter
      • Name: StringInverter
    • Add the method definition String invert(String input);
    package org.fipro.inverter;

    public interface StringInverter {

        String invert(String input);

    }

Hint:
You can also copy the above snippet and paste it in Eclipse when having the src folder of the project selected in the Project Explorer. This will automatically create the package and the source file at the correct place.

  • Create the package-info.java file in the org.fipro.inverter package.
    • Right click on the package org.fipro.inverter → New → File
    • Set File name to package-info.java
    • Click Finish
    • Copy the following code into the editor and save the file
      @org.osgi.annotation.bundle.Export(substitution = org.osgi.annotation.bundle.Export.Substitution.NOIMPORT)
      @org.osgi.annotation.versioning.Version("1.0.0")
      package org.fipro.inverter;
    

    This configures that the package is exported. If this file is missing, the package is a Private-Package and therefore not usable by other OSGi bundles. The substitution parameter avoids that the package is used as import inside the same bundle.

Bndtools vs. PDE

With Bndtools you create a Bnd OSGi Project. Additionally you need to create a configuration project if you don’t have one yet. Typically this is called the cnf project that contains workspace-wide configurations, like the repositories that are used to resolve the dependencies. They have their own workspace concept additionally to the Eclipse workspace, and they have workspace templates that help with the creation.

PDE uses the concept of a Target Platform, where you specify the repositories and the bundles that are used to create an application. The target platform typically contains p2 repositories and the Eclipse IDE itself can be used as target platform for the development. In a professional environment you typically create a project specific target definition in a separate project (I explained that in my basic recipe). So conceptually the usage of a repository or configuration project is similar when comparing PDE and Bndtools. But as Bndtools doesn’t support p2 and the target platform concept of PDE, the cnf project is different and required for OSGi development with Bndtools.

In Bndtools you configure the bundle in the bnd.bnd file. With the Automatic Manifest Generation PDE project layout, you also configure the bundle in a .bnd file. So the project layouts are similar. All informations for the bundle are configured there, like dependencies, bundle version and bundle symbolic name for example. The MANIFEST.MF file is generated with the correct information. The creation of the MANIFEST.MF file is a major advantage of Bndtools compared to a classical PDE project layout, where the MANIFEST.MF needs to be created and maintained manually, as this is one of the more complicated things regarding OSGi.

Another difference is that the resulting bundle JAR file is automatically generated. After saving the bnd.bnd file, the bundle jar is available for usage in the generated folder.

Interlude: Project Layout Migration

The following sections describe how to create new OSGi bundle projects with the Automatic Manifest Generation project layout. If you have an existing project in the classical PDE plug-in project layout, you can also use a wizard to convert it to the Automatic Manifest Generation project layout.

  • Right click on project → Plug-in Tools → Convert to Automatic Manifest Generation…
  • Configure how the project should be migrated (or keep the defaults)
  • Click Finish

The resulting pde.bnd file might contain more information than necessary. For example the Component Descriptor XML files that were generated by the DS Annotation Support still exist and are referenced. If you used DS annotations, these files can be safely deleted and the Service-Component entry in the pde.bnd can be removed.

4. Service Provider

After the API bundle is specified, a service provider bundle with a service implementation can be created.

4.1. Create and configure the service provider bundle

In the Plug-in Perspective create a new Plug-in Project via File -> New -> Plug-in Project. Choose a name that indicates that this is a bundle that provides a service implementation (e.g. org.fipro.inverter.provider), and on the following wizard page ensure that no Activator is generated, no UI contributions will be added and that no Rich Client Application is created. The steps are the same as for creating the API bundle.

  • Create a new plug-in project
    • Main Menu → File → New → Plug-in Project
    • Set name to org.fipro.inverter.provider
    • In the Target Platform section select
      • This plug-in is targeted to run with: an OSGi framework:
      • Select standard in the combobox
      • Check Generate OSGi metadata automatically
    • Click Next
    • Set Name to Inverter Provider
    • Select Execution Environment JavaSE-17
    • Ensure that Generate an Activator and This plug-in will make contributions to the UI are disabled
    • Click Finish
    • If you do not see the tabs at the bottom of the recently opened editor with name org.fipro.inverter.provider, close the editor and open the pde.bnd file in the project org.fipro.inverter.provider.
      • Switch to the pde.bnd tab
        • Add the Bundle-ActivationPolicy to get the bundle automatically started in an Equinox runtime
        • Add the -runee instruction to create the requirement on Java 17
        • Add the -buildpath instruction to specify the dependency to the API bundle
            Bundle-Name: Inverter Provider
            Bundle-SymbolicName: org.fipro.inverter.provider
            Bundle-Vendor: 
            Bundle-Version: 1.0.0.qualifier
            Bundle-ActivationPolicy: lazy
            -runee: JavaSE-17
            -buildpath: \
            org.fipro.inverter.api
          

Note:
Although there is a Dependencies tab for the opened pde.bnd editor, you should not manage the dependencies there. Changes result in additional Require-Bundle or Import-Package statements in the pde.bnd file, or even cause errors. You should always use the -buildpath instruction to add build-time dependencies.

Note:
In Eclipse projects you also often find bundle names that end with .impl instead of .provider. IMHO suffixing such bundles with .provider makes more sense, but in the end it is a project decision.

4.2 Create the service implementation

  • Main Menu → File → New → Class
    • Source Folder: org.fipro.inverter.provider/src
    • Package: org.fipro.inverter.provider
    • Name: StringInverterImpl
    • Interfaces: org.fipro.inverter.StringInverter
  • Implement the method String invert(String);
  • Add the org.osgi.service.component.annotations.Component annotation on the class
package org.fipro.inverter.provider;

import org.fipro.inverter.StringInverter;
import org.osgi.service.component.annotations.Component;

@Component
public class StringInverterImpl implements StringInverter {

    @Override
    public String invert(String input) {
        return new StringBuilder(input).reverse().toString();
    }
}

The usage of the @Component annotation is the important thing in this step. It is to identify the class as a Service Component and is used to generate the Component Description.

Note:
With the classical PDE plug-in project layout, the usage of the @Component annotation triggers the generation of the Component Description on save (if you have the DS Annotations Support enabled). You will find the XML file in the OSGI-INF folder in the org.fipro.inverter.provider project, which also has been created if it didn’t exist before (note that in PDE the file is also called Component Definition, just in case you are getting confused by names). It also updates the MANIFEST.MF file by adding (or updating) the Service-Component header to point to the Component Description. This is necessary for the SCR to find, load and process the Component Description.  The build.properties file is updated aswell to include the Component Description file. Unfortunately this generates a warning saying the OSGI-INF folder itself is not included. To remove that warning you can simply open the build.properties file and add the OSGI-INF folder itself. This is at least sufficient for this simple example.

There is nothing more to do at this point. You will find a more detailed description on the @Component annotation at the end of this tutorial.

Note:
The fact that after code generation warnings are shown is IMHO really annoying. I suggested to always add the whole OSGI-INF folder to the build.properties, but this was declined with the comment that not everybody wants to always add all files in that folder to the resulting bundle JAR. This is of course a valid remark. I therefore created another ticket to either rethink that warning (not sure if that warning is really valid) or if it should be possible to disable that warning somehow. There has been no action on that topic while writing this tutorial, but you can follow the discussion in Bug 491666.

Bndtools vs. PDE

  • With Bndtools you create a new Bnd OSGi Project with a Provider Bundle template for example.
  • Dependencies are managed via the bnd.bnd file. If you open that file with the Bnd Project Editor, you can add the build time dependency to the API bundle on the Build tab in the Build Path section, which will be used to calculate the necessary imports in the MANIFEST.MF file.

As explained before, the advantage is clearly the generation of the OSGi meta-data in the MANIFEST.MF file.

Interlude: Capabilities

There is no need to specify a bundle dependency to the SCR on package level. But without specifying such a dependency, the OSGi framework doesn’t know about that dependency when resolving the provider bundle. As we don’t need to import a package from an SCR implementation, we need another way to specify the dependency. In OSGi this is done via Requirements and Capabilities. Since the DS 1.3 specification the SCR needs to provide the following capability:

Provide-Capability: osgi.extender;
 osgi.extender="osgi.component";
 version:Version="1.3";
 uses:="org.osgi.service.component"

A provider bundle can require the osgi.extender capability which wires the bundle to the SCR implementation and ensures that both use the same version of the org.osgi.service.component package. This can be done by adding the following header to the bundle MANIFEST.

Require-Capability: osgi.extender;
 filter:="(&(osgi.extender=osgi.component)(version>=1.3)(!(version>=2.0)))"

Note:
The PDE Plug-in Manifest Editor has not build in support for specifying capabilities. You therefore have to add that header in the MANIFEST.MF source tab manually if you use the classical PDE plug-in project layout.

You could also provide the osgi.service capability to make consumers able to declare a dependency on the service that is provided. As per specification this would look like the following snippet for the service of this tutorial:

Provide-Capability: osgi.service;
 objectClass:List<String>="org.fipro.inverter.StringInverter"

On the consumer side you could then require the osgi.service capability like this:

Require-Capability: osgi.service;
 filter:="(objectClass=org.fipro.inverter.StringInverter)";effective:=active

You should notice the effective:=active directive here. It is necessary so the OSGi Framework will resolve the bundle without checking if another bundle provides that capability. Without that directive or setting effective:=resolve the resolution of the bundle would be prevented. 

Of course, using the Automatic Manifest Generation the capability headers are also generated. You can verify this by opening the pde.bnd files of the generated projects and open the MANIFEST.MF tab.

Bndtools vs. PDE

With Bndtools the capability headers are automatically generated. It always provides the osgi.service capability. And it will require the osgi.extender capability in case the component XML file is generated for DS spec 1.3.

5. Service Consumer

To show that our service is working, we need to implement a consumer. As an Eclipse developer it would be natural to create a small Eclipse RCP application for that. But I want to stick to the Bndtools tutorial and just create a console command for that. This is also easier to achieve without adding Eclipse RCP specifics to this tutorial.

Note:
The Eclipse OSGi console is based on the Felix Gogo Shell. We can therefore easily use the same approach for creating a console command as shown in the Bndtools tutorial.

5.1 Create and configure the service consumer bundle

  • Create a new plug-in project
    • Main Menu → File → New → Plug-in Project
    • Set name to org.fipro.inverter.command
    • In the Target Platform section select
      • This plug-in is targeted to run with: an OSGi framework:
      • Select standard in the combobox
      • Check Generate OSGi metadata automatically
    • Click Next
    • Set Name to Inverter Command
    • Select Execution Environment JavaSE-17
    • Ensure that Generate an Activator and This plug-in will make contributions to the UI are disabled
    • Click Finish
    • If you do not see the tabs at the bottom of the recently opened editor with name org.fipro.inverter.command, close the editor and open the pde.bnd file in the project org.fipro.inverter.command.
      • Switch to the pde.bnd tab
        • Add the Bundle-ActivationPolicy to get the bundle automatically started in an Equinox runtime
        • Add the -runee instruction to create the requirement on Java 17
        • Add the -buildpath instruction to specify the dependency to the API bundle
            Bundle-Name: Inverter Command
            Bundle-SymbolicName: org.fipro.inverter.command
            Bundle-Vendor: 
            Bundle-Version: 1.0.0.qualifier
            Bundle-ActivationPolicy: lazy
            -runee: JavaSE-17
            -buildpath: \
            org.fipro.inverter.api
          

5.2 Create the service consumer class

  • Main Menu → File → New → Class
    • Source Folder: org.fipro.inverter.command/src
    • Package: org.fipro.inverter.command
    • Name: StringInverterCommand
  • Add the org.osgi.service.component.annotations.Component annotation on the class
    • Specify configuration properties that are needed for the configuration of the command: osgi.command.scope and osgi.command.function, so our command can be triggered by executing fipro:invert on the console. The method that gets triggered needs to have the same as the configured function property (invert in our case).
    • Specify the service attribute to register the command as a service. The component would be otherwise an Immediate Component, because we don’t implement an interface.
  • Consume a StringInverter via dependency injection.
    • Add a member inverter of type StringInverter and use the @Reference annotation on that field to bind to the service via Field Injection
  • Implement the method void invert(String);
package org.fipro.inverter.command;

import org.fipro.inverter.StringInverter;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;

@Component(
    property= {
        "osgi.command.scope::String=fipro",
        "osgi.command.function:String=invert"
    },
    service=StringInverterCommand.class
)
public class StringInverterCommand {

    @Reference
    private StringInverter inverter;

    public void invert(String input) {
        System.out.println(inverter.invert(input));
    }
}

Further information on the annotations can be found at the end of the tutorial.

Bndtools vs. PDE

In the bnd.bnd file of the command project you need to switch to the Build tab and add

  • org.fipro.inverter.api

6. Run

Now that our bundles are ready we can launch an OSGi application to test it. For this create a launch configuration, to be able to start directly from the IDE.

  • Main Menu → Run -> Run Configurations…
  • In the tree view, right click on the OSGi Framework node and select New from the context menu
  • Specify a name, e.g. OSGi Inverter
  • Bundles tab
    • Click Deselect All
    • Select the following bundles
      • org.fipro.inverter.api
      • org.fipro.inverter.command
      • org.fipro.inverter.provider
      • org.apache.felix.gogo.command
      • org.apache.felix.gogo.runtime
      • org.apache.felix.gogo.shell
      • org.apache.felix.scr
      • org.eclipse.osgi
      • org.osgi.service.component
      • org.osgi.util.function
      • org.osgi.util.promise
    • Ensure that Default Auto-Start is set to true
  • Arguments tab
    • Remove -consoleLog -console from the Program Arguments
    • Add -Dosgi.console= to the VM Arguments
  • Click Run

The Console view should open with a g! prompt. Now you can type in the invert command with a parameter and see the result. Note that it is not necessary to specify the scope here, because there is no other invert command published.

The result should look similar to this:

g! invert Simpson
nospmiS
g! 

Note:
The Eclipse Launcher has also a feature that allows to only configure the minimal set of bundles and let the additionally required bundles be resolved automatically. This makes it much more comfortable to create a Run Configuration but could also result in having more bundles in the runtime than needed.

Bndtools vs. PDE

You will find a launch.bndrun file in the command bundle project, that can be used to launch our small OSGi application. The OSGi Framework and the Execution Environment needs to be selected and the org.fipro.* bundles need to be added to the Run Requirements. Click on Resolve so the Run Bundles are automatically determined. After that you simply click on Run OSGi in the upper right corner of the editor and the application starts up, opening the Console view for interaction.

The creation of the launch configuration in Bndtools feels more comfortable than the one with PDE. On the other hand I had sometimes strange issues with that editor. It sometimes forgot my changes in the Run tab on save, which left me editing the launch configuration in the Source tab directly. This happened mostly when trying to create a launch configuration with Equinox as OSGi Framework.

7. DS Annotations

That’s it with the tutorial. Now let’s have a closer look at the DS annotations.

7.1 @Component

Let’s start with the @Component annotation. It is the first Declarative Service Annotation we used. It indicates that the annotated class is intended to be an OSGi component and triggers the generation of the Configuration Description as explained above. If nothing else is specified, the annotation defaults will create the easiest to use component:

  • Its name is the full qualified class name
  • It registers all of the class’s directly implemented interfaces as services
  • The instance will be shared by all bundles
  • It is enabled
  • It is immediate if it has no services, otherwise it is delayed
  • It has an optional configuration policy
  • The configuration PID is the full qualified class name

These defaults can be changed via annotation type elements (or annotation parameter if that is better understandable). The following elements are supported by the @Component annotation:

Annotation Type Element Description
configurationPid The configuration PID that is used for the configuration of the component in conjunction with the ConfigurationAdmin.
configurationPolicy Configure whether a configuration object for the component needs to be present via ConfigurationAdmin.
enabled The initial enabled state of the component when the containing bundle is started.
factory The factory identifier for this component, indicating that this component is a Factory Component.
immediate Control whether a component configuration should be immediately activated after becoming satisfied or if the activation should be delayed. Needs to be false in case the factory attribute is set also, needs to be true if no service is provided.
name The name of the component which needs to be unique within a bundle.
properties References a Java Properties File entry in the bundle. The entry is read and processed to obtain the properties and their values.
property Array of Strings to specify a set of component properties. Can be seen as additional meta-information of a component that is used for configuration. It is a number of key-value pairs in the format <name>(:<type>)?=<value> where the type information is optional and defaults to String. The following example defines a property with key answer and an Integer value 42: answer:Integer=42
service The name(s) of the interface or class this component is registered under as a service. Needs to be a full qualified class name.
servicefactory By setting this value to true, a new Component Instance will be created for each distinct bundle that request the service. Note: this element is replaced by scope with DS 1.3
xmlns The XML name space of the Component Description for this component. By default the lowest Declarative Services XML name space is used, that supports all the specification features used by this component.

Note:
The configuration related type elements will be explained in an upcoming post that will focus on the ConfigurationAdmin.

With DS 1.3 the following elements were added to the @Component annotation:

Annotation Type Element Description
reference Array of @Reference annotations with specified name and service elements. Used to accesss references via Lookup Strategy.
scope The scope of the provided service. SINGLETON by default which means the Component Instance will be used by all bundles. BUNDLE if every bundle should get its own Component Instance. PROTOTYPE to create a new Component Instance for each distinct request. Note: This element replaces the servicefactory element.

With DS 1.4 the following elements were added to the @Component annotation:

Annotation Type Element Description
factoryProperty Array of Strings to specify a set of factory properties for a Factory Component.
factoryProperties References a Java Properties File entry in the bundle. The entry is read and processed to obtain the factory properties and their values.

7.2 @Activate, @Deactivate, @Modified

Although we don’t use them in the example, I want to show and explain the life cycle annotations. A component can specify methods that should be called when a life cycle event happens. This is the activation, modification or deactivation of a component. Using the DS annotations you can mark a method as such a life cycle method.

Annotation Description
@Activate The method that should be called on component activation.
@Modified The method that should be called if a configuration is updated using the ConfigurationAdmin.
@Deactivate The method that should be called on component deactivation.

These methods can have zero or more arguments, where each argument must be one of the following types:

  • ComponentContext
    The component context for the component configuration.
  • BundleContext
    The bundle context of the components bundle.
  • Map<String, ?>
    An unmodifiable map containing the component properties.

The following snippet is an example for an activate method that takes all supported parameters:

@Activate
private void activate(
    ComponentContext c,
    BundleContext b,
    Map<String, ?> properties) {

    //do some initialization stuff
}

@Deactivate accepts an additional int or Integer parameter for the deactivation reason. The different deactivation reasons are specified in org.osgi.service.component.ComponentConstants which is contained in the SCR implementation.

With DS 1.3 there is an additional parameter type that is accepted by the life cycle methods. It is called a Component Property Type, which allows type safe access to component properties. You can also access the component properties via the Map parameter, but in an unsafe manner. You need to check the type and cast accordingly. As the component properties can come from anywhere (specified via @Component property or properties element, via ConfigurationAdmin or factory component) it is nicer to have a type safe access via Component Property Type, as the framework will do the conversion. As this tutorial is about getting started with Declarative Services, I will not cover this here in more detail. Have a look at the Configuring OSGi Declarative Services blog post for further information.

Note that you can only use each annotation once in your component implementation. There is no overriding capability but a strict order which method will be chosen in such a case. I therefore suggest to only add one method per life cycle annotation and choose the parameter list that matches the best.

The OSGi best practices suggest to not use the public access modifier for the life cycle methods. They are only intended to be called by the SCR and should not be called from other code.

With DS 1.4 the @Activate annotation can also be used to

  • Get the activation objects via field injection
    @Activate
    private ComponentContext c;
    @Activate
    private BundleContext b;
    @Activate
    private Map<String, Object> properties;
    
  • Mark a constructor to be used by SCR for Constructor Injection

7.3 @Reference

The last available annotation is @Reference. It is used to specify the dependency on other services. With DS 1.2 it can only be used with Event Methods. DS 1.3 also introduced the usage of @Reference on fields and the type element reference of @Component. Since DS 1.4 it is also possible to use @Reference on parameters of a constructor to support Constructor Injection.

Let’s first explain Event Methods. An Event Method is called by the SCR when a service becomes bound, unbound or its configuration properties are updated. With DS 1.2 it is necessary to bind a service reference via bind event method. With DS 1.3 Event Methods are still useful in case actions need to be performed if a service is bound, unbound or changes its configuration. There are three Event Methods per service reference:

  • bind
    Called to bind a new service to the component. For static references this method is called before the activate method. For dynamic references this method can also be called while the component is active.
  • updated
    Called when the configuration properties of a bound service are modified.
  • unbind
    Called when the SCR needs to unbind the service. For static references this method is called after the deactivate method. For dynamic references this method can also be called while the component is active.

With DS 1.2 the bind method is mandatory to specify the reference. The updated and unbind methods are optional as per specification, although specifying the unbind method is often recommended (see below).

The @Reference annotation needs to be applied on the bind event method. The following defaults are used in that case:

  • The name of the bind method is used for the name of the reference. That means the method name after the prefix (e.g. setStringInverter() -> StringInverter). Mind the case sensitivity, as the name in that case starts with an upper case letter.
  • 1:1 cardinality.
  • Static reluctant policy.
  • The requested service is the type of the first argument of the bind method.
  • It will infer a default unset method and updated method based on the name of the bind method.

The defaults can be changed via annotation type elements, which will be explained shortly.

Note:
When using Event Methods for reference binding, typically the bind method is used to store the service reference instance in a field. In case of a dynamic reference there should be always an unbind method to clean up such a reference. This is necessary as for dynamic references the reference binding/unbinding can happen while the Component Configuration is ACTIVE. The unbind method is not necessary for static references, as a Component Configuration would be deactivated if the bound service is not available anymore, and activated again if another target service could be bound.

Event Methods accept different parameters. With DS 1.2 these can be:

  • ServiceReference
    The service reference to the bound service, which can later be used for the Lookup Strategy.
  • Service Type
    The type of the referenced service, so the Service Instance itself is passed.
  • Service Type + Map<String, ?>
    The Service Instance itself is passed together with the unmodifyable map that contains the configuration properties of the bound service.

The following snippet shows the bind method for the StringInverter reference, that also takes the configuration properties as parameter:

@Reference
void bindStringInverter(
        StringInverter inverter, Map<String, ?> properties) {
    // check the properties for some initialization
    this.inverter = inverter;
}

The bind event methods are typically prefixed with either bind, set or add. The unbind method should have the corresponding prefixes unbind, unset or remove, so they are automatically picked up by @Reference. The updated event method follows that principle by replacing the bind prefix with updated. Alternatively it is possible to explicitly specify the unbind and the updated event methods via type element.

With DS 1.3 an additional parameter is supported for usage with the PROTOTYPE scope, ComponentServiceObjects. Also additional parameter combinations are supported with DS 1.3. So it is for example possible to only get the map of configuration properties injected, or a combination of ServiceReference, Service Type, ComponentServiceObjects and the map of configuration properties. Additional information about scopes and ComponentServiceObjects in DS 1.3 can be found in my Control OSGi DS Component Instances blog post.

Also introduced with DS 1.3 is the Field Strategy for binding services. With this it is not necessary to specify a bind event method for a reference. Instead it is possible to apply the @Reference annotation to a field in the component implementation class. For a static reference the field will be set before the component instance is activated. Dynamic references need to be marked as volatile so that changes to that field are also visible to other threads.

When applying @Reference on a field, the following defaults are used:

  • The name of the field is used for the name of the reference.
  • 1:1 cardinality if the field is not a collection. 0..n cardinality if the field is a collection.
  • Static reluctant policy if the field is not declared volatile. Dynamic reluctant policy if the field is declared volatile.
  • The requested service is the type of the field in case the field type is a service type.

The @Reference annotation can be applied to different field types. The following list shows the supported types for references with unary cardinality:

  • Service Type
    The type of the referenced service, so the Service Instance itself is passed.
  • ServiceReference
    The service reference to the bound service, which can later be used for the Lookup Strategy.
  • ComponentServiceObjects
    The actual service object or objects for services with prototype scope.
  • Map<String, ?>
    The unmodifyable map that contains the configuration properties of the bound service.
  • Map.Entry<Map<String, ?>, Service Type>
    An unmodifiable Map.Entry whose key is an unmodifiable Map containing the service properties of the bound service, as above, and whose value is the bound service object.

The cardinality of the reference is determined by the field type. If multiple service references should be bound (or at least one service reference) one of the following types needs to be used, where the type of objects in the collection can be one of the above:

  • Collection
  • List
  • A subtype of Collection
    This can only be used for dynamic references using the UPDATE field-option. The collection object needs to be initialized in the component instances constructor.

Note:
Only instance fields are supported. The @Reference annotation can not be applied to static fields.

Since DS 1.4 the @Reference annotation can also be used for Constructor Injection when it is applied to a constructor parameter.

Since DS 1.5 it is possible to use Optional to hold unary references for field and constructor injection. This is primarily for optional (0..1) cardinality but can also be used with mandatory (1..1) cardinality. And it is possible to use the special interface name org.osgi.service.component.AnyService to select the target services. This means that a service type is not used to select the target services and just the target property is used to select the target services. When the special interface name of org.osgi.service.component.AnyService is used, a target property must be present to constrain the target services to some subset of all available services. A more detailed description on this can be found in the Configuring OSGi Declarative Services blog post.

There are various options to configure service references. The following annotation type elements are supported by the @Reference annotation for configuration:

Annotation Type Element Description
cardinality Specify if the reference is optional and if single or multiple bound services are supported. Supported values are specified in the ReferenceCardinality enum: <ul><li>0..1 = OPTIONAL</li> <li>1..1 = MANDATORY</li> <li>0..n = MULTIPLE</li> <li>1..n = AT_LEAST_ONE</li></ul>
name The component local name of the reference which can be used for the Lookup Strategy.
policy The reference policy regarding dynamicity. Supported values are specified in the ReferencePolicy enum: <ul><li> STATIC
The component instance doesn’t see any dynamics. Dependent on the policy-option it is either ignored that a new or higher ranking service becomes available, or the Component Configuration is deactivated and reactivated.</li> <li>DYNAMIC
The SCR can change the set of bound services without deactivating the Component Configuration.</li></ul>
policyOption Specify the binding behavior when a new, potentially with a higher ranking, target service becomes available. Supported values are specified via ReferencePolicyOption enum:<ul><li>RELUCTANT
In case of OPTIONAL or MANDATORY cardinality, the new service is ignored for both STATIC and DYNAMIC policy (except if a service comes up for an OPTIONAL reference that is not bound yet). In case of MULTIPLE or AT_LEAST_ONE cardinality, the new service is ignored for the STATIC policy, and simply bound with the DYNAMIC policy.</li> <li>GREEDY
In case of STATIC references a new service causes reactivation for any cardinality. In case of DYNAMIC references a binding is triggered. For OPTIONAL and MANDATORY this means rebinding in case there is already a bound service.</li></ul>
service The full qualified class name of the referenced service. Typically this type element is not specified manually!
target OSGi Framework filter expression to constrain the set of target services. It uses the LDAP Filter Syntax and is empty by default.
unbind The name of the unbind method.
updated The name of the updated event method, which is used to notify that a bound service has modified its  properties.

With DS 1.3 the following element types are added:

Annotation Type Element Description
bind The name of the bind event method. Can be used to combine Field Strategy and Event Strategy, for example to only get the configuration properties map in the bind method.
field The name of the field in the component implementation class that is used to hold the bound services. Typically this type element is not specified manually!
fieldOption Specify how the field value needs to be managed with the Field Strategy. Supported values are specified via FieldOption enum:<ul><li>REPLACE
SCR must set the field value. Values set by the constructor are overwritten. This is the default value. Static references and unary dynamic references must use this option.</li> <li>UPDATE
SCR must update the collection set in the field. It will update the content, not replace the collection instance. This way it is possible to choose the Collection implementation that should be used to track the service references. Can only be used for dynamic references with MULTIPLE or AT_LEAST_ONE cardinality.</li></ul>
scope  The reference scope for this reference. Supported values are specified via ReferenceScope enum:<ul><li>BUNDLE
All activated components within a bundle must uses the same service object.</li> <li>PROTOTYPE
Each activated component instance may use a single, distinct service object.</li> <li>PROTOTYPE_REQUIRED
Each activated component instance must use a single, distinct service object.</li></ul>

With DS 1.4 the following element types are added:

Annotation Type Element Description
parameter The zero-based parameter number of the constructor parameter for this reference.
collectionType The collection type for this reference.

Note:
The so called Target Property of a reference, to filter by properties additionally to the service interface, can also be configured dynamically via ConfigurationAdmin, which I will cover in a follow up blog post.

At last here are some snippets to show the usage of @Reference with different techniques:

Event Strategy / Method Injection

@Component(
    property= {
        "osgi.command.scope:String=fipro",
        "osgi.command.function:String=invert"},
    service=StringInverterCommand.class
)
public class StringInverterCommand {

    private StringInverter inverter;

    @Reference
    void setStringInverter(StringInverter inverter) {
        this.inverter = inverter;
    }

    // Note: static reference, therefore no unbind method needed

    public void invert(String input) {
        System.out.println(inverter.invert(input));
    }
}

Lookup Strategy (DS 1.2)

@Component(
    property= {
        "osgi.command.scope:String=fipro",
        "osgi.command.function:String=invert"},
    service=StringInverterCommand.class
)
public class StringInverterCommand {

    private ComponentContext context;
    private ServiceReference<StringInverter> reference;

    @Activate
    void activate(ComponentContext context) {
        this.context = context;
    }

    @Reference
    void setStringInverter(ServiceReference<StringInverter> reference) {
        this.reference = reference;
    }

    public void invert(String input) {
        StringInverter inverter =
            context.locateService("StringInverter", reference);
        if (inverter != null) {
            System.out.println(inverter.invert(input));
        } else {
            System.out.println("StringInverter not available!");
        }
    }
}

Lookup Strategy (DS 1.3)

@Component(
    property= {
        "osgi.command.scope:String=fipro",
        "osgi.command.function:String=invert"},
    service=StringInverterCommand.class,
    reference=@Reference(name="inverter", service=StringInverter.class)
)
public class StringInverterCommand {

    private ComponentContext context;

    @Activate
    void activate(ComponentContext context) {
        this.context = context;
    }

    public void invert(String input) {
        StringInverter inverter =
            (StringInverter) context.locateService("inverter");
        if (inverter != null) {
            System.out.println(inverter.invert(input));
        } else {
            System.out.println("StringInverter not available!");
        }
    }
}

Field Injection (DS 1.3)

@Component(
    property= {
        "osgi.command.scope:String=fipro",
        "osgi.command.function:String=invert"},
    service=StringInverterCommand.class
)
public class StringInverterCommand {

    @Reference
    private StringInverter inverter;

    public void invert(String input) {
        System.out.println(inverter.invert(input));
    }
}

Constructor Injection (DS 1.4)

@Component(
    property= {
        "osgi.command.scope:String=fipro",
        "osgi.command.function:String=invert"
    },
    service=StringInverterCommand.class
)
public class StringInverterCommand {

    private StringInverter inverter;
    
    @Activate
    public StringInverterCommand(@Reference StringInverter inverter) {
    	this.inverter = inverter;
    }

    public void invert(String input) {
        System.out.println(inverter.invert(input));
    }
}

Finish

Now I’m done with this blog post. It has become much longer than I initially planned, but well, there are also a lot of information I gathered the last months. And I still haven’t blogged about everything. Next up will be the configuration of components via ConfigurationAdmin and the deployment of the components with different OSGi implementations.

I hope you enjoyed following my  blog post and it gives you the necessary information to get started with OSGi Declarative Services (in Eclipse and plain OSGi). If you find something incorrect it would be great if you contact me, so I am able to update the information accordingly.

The sources for this tutorial are hosted on GitHub:

In the following list you can find most of the links to resources that I used to gather the information in this tutorial. As I come across different blog posts, forum discussions, mailing lists, Google Groups and Stackoverflow posts, I can not link all of them.

Updated: