LibraryPrintFeedback

Managing OSGi Dependencies

Version 7.1

December 2012
Trademark Disclaimer
Third Party Acknowledgements

Updated: 08 Jan 2014

Table of Contents

1. Bundle Class Loader
Class Loader Basics
Conflicting Classes
Class Loading Algorithm
2. Importing and Exporting Packages
Overview of Bundle Types
Sample OSGi Application
Library Bundle
API Bundle
Provider Bundle
API/Provider Combination
API/Provider Build-Time Combination
Consumer Bundle
3. Versioning
Semantic Versioning
Export Versioning
Automatic Import Versioning

List of Figures

2.1. Relationship between API, Provider, and Consumer Bundles
2.2. Sample OSGi Application
3.1. Consumers and Provider of API Interfaces

List of Tables

2.1. Import/Export Rules for Bundles
3.1. Example Consumer Import Ranges
3.2. Example Provider Import Ranges

List of Examples

1.1. Symbolic Reference Used in a Class Cast
2.1. Maven Bundle Plug-In Settings
2.2. Plug-In Settings for API/Provider Combination
2.3. Plug-In Settings for Build-Time Combination

One of the key class loader concepts is class loader delegation, which is where a class loader, instead of loading a class itself, asks another class loader to load the class. When class loader delegation occurs, you need to distinguish between:[1]

Initial class loader

The class loader that is initially asked to load the class.

Effective class loader

The class loader that actually loads the class, after delegation is taken into account.

To explain the difficulties that can be caused by having multiple copies of a loaded class, we need the concept of a symbolic reference to a class or interface. A symbolic reference is a point in your code where a class name is used, except the actual definition of the class. For example, when a class name is used to specify a return type, a parameter type, or a variable type, the class name is a symbolic reference.

At run time, the JVM must resolve each symbolic reference by linking it to a specific instance of a loaded class. Symbolic references are resolved using the same class loader as the class in which they appear. For example, Example 1.1 shows the definition of the TestHello class, which is loaded by the class loader, A.


[Important]Important

Although the symbolic reference, org.foo.Hello, is initially loaded by class loader A, this does not imply that the symbolic references must be resolved to A/org.foo.Hello. If the initial class loader A decides to delegate to another class loader, B, the symbolic reference could be resolved to B/org.foo.Hello instead (so that B is the effective class loader). The delegation mechanism is crucial, because it enables an OSGi bundle to re-use existing loaded classes and avoid class space inconsistencies.

When multiple class loaders are used in parallel (as happens in OSGi), there is a danger that a class could be loaded more than once. This is undesirable, because it almost inevitably causes class cast exceptions at run time.

For example, consider the TestHello class shown in Example 1.1, where the org.foo.Hello symbolic reference has been resolved to A/org.foo.Hello. If you also have a Hello object that is an instance of B/org.foo.Hello type, you will get a class cast exception when you pass this object to the TestHello.useHelloObj(Object) method. Specifically, in the line of code that performs the following cast:

org.foo.Hello h = (org.foo.Hello) hello;

The org.foo.Hello symbolic reference has been resolved to the A/org.foo.Hello type, but the hello object is of B/org.foo.Hello type. Because the types do not match, you get the class cast exception.

The following is a simplified description of the bundle class loading algorithm (for example, it does not enumerate all of the ways in which class loading can fail). For a full description of the algorithm, see the Runtime Class Loading section of the OSGi Core Specification.

  1. If the class belongs to one of the java.* packages or any packages listed in the org.osgi.framework.bootdelegation property, the bundle class loader delegates to the parent class loader.

  2. If the class belongs to one of the packages listed in Import-Package, the bundle class loader delegates loading to the corresponding exporter bundle.

  3. If the class belongs to one of the packages imported by Require-Bundle, the bundle class loader delegates loading to the corresponding exporter bundle.

    [Note]Note

    It is strongly recommended that you avoid using the Require-Bundle header. OSGi dependencies are meant to be defined at package granularity, not bundle granularity.

  4. Next, the bundle class loader looks for the class amongst its internal classes (inside its own JAR file).

  5. Next, the bundle class loader searches the internal classes of any fragments attached to the bundle.

  6. Finally, if the class belongs to one of the packages imported using DynamicImport-Package, the bundle class loader delegates loading to the corresponding exporter bundle (if there is one).



[1] Terminology coined by Andreas Schaefer, an O'Reilly Java author.

We can identify the following typical bundle types:

Library bundle

A library bundle contains Java classes and interfaces, which are public and intended to be used by other bundles. Often, in a library, there is no formal separation between API interfaces and implementation classes. Instead, developers simply instantiate and use the classes provided by the library.

A library bundle does not publish or access any OSGi services.

API bundle

A pure API bundle contains only Java interfaces (or abstract classes), which are public. The implementation of the API is meant to be provided in a separate bundle or JAR.

An API bundle does not publish or access any OSGi services.

Provider bundle

A provider bundle contains the classes that implement an API. Usually, the classes in a provider bundle are all private and are not exported from the bundle.

The natural mechanism for a provider bundle to expose its functionality is to create and publish one or more OSGi services (where the OSGi services are then accessed through the public API).

Consumer bundle

A consumer bundle contains code that uses an API.

A consumer bundle typically accesses one or more OSGi services; it does not usually publish an OSGi service itself (unless it is acting as a hybrid of a consumer and a provider).

API/provider combination bundle

In some cases, it can make sense to package API packages and implementation packages together, resulting in an API/provider combination bundle. For example, if you intend to provide only one implementation of an API, it can be simpler to package the API and its implementation in a single bundle.

For the API/provider combination bundle, it is assumed that the API code and the implementation code belong to the same Maven project.

API/provider build-time combination bundle

Even if you opt for a formal separation of API code and implementation code—that is, where the API code and the implementation code are developed in separate Maven projects—you might nevertheless want to add the API interfaces to the provider bundle. Peter Kriens (creator of the Bnd tool, on which the Maven bundle plug-in is based) recommends that a provider bundle should always include its API interfaces. One reason for this is that, when you deploy a provider bundle without embedding the API, there always a risk that the required version of the API will not be available in the OSGi container at deploy time. By embedding the API interfaces in the provider, you eliminate that risk and improve the reliability of your deployment.

When you develop your API interfaces and your implementation classes in separate Maven projects, the bundle plug-in supports a special configuration that enables you to embed the API in the provider bundle at build-time. Hence, this bundle is called an API/provider build-time combination.

An OSGi service is essentially a plain Java object, made accessible to other bundles by being published in the OSGi service registry (see Exporting a Service in Deploying into the Container).

Provider bundles demonstrate a typical use case of an OSGi service. The classes in a provider bundle are only meant to be accessed indirectly—that is, through the relevant API interfaces. But there must be some mechanism that enables you to instantiate one or more of the implementation classes, in order to bootstrap the implementation. Typically, the mechanism for bootstrapping an implementation depends on the framework or container you are currently using. For example, in Spring you would typically use a bean element to create an instance of an implementation class.

In the context of OSGi, however, the natural mechanism for bootstrapping an implementation is to create and publish one or more OSGi services.

Example 2.1 shows a minimal configuration of the Maven bundle plug-in, which you can use for basic Maven projects.


Although the preceding minimal configuration is not ideal for all bundle projects, it does have a useful default behavior that is often good enough to get your project started:

Figure 2.2 shows an overview of a sample OSGi application consisting of five bundles, which illustrates the basic bundle types discussed in the previous section. The application is driven by the hello-consumer bundle, which imports a HelloParis service from the hello-paris bundle and imports a HelloBoston service from the hello-boston bundle.


The sample OSGi application consists of the following bundles:

time-util

Fits the pattern of a library bundle. The time-util bundle is a utility library that can create Clock instances that tell the time in a particular time zone.

The time-util bundle is implemented using classes from the JDK and thus has no external package dependencies.

hello-paris

Fits the pattern of an API bundle. The hello-paris bundle consists of a single Java interface, which returns a greeting, getGreeting(), and gives the local time in Paris, getLocalTime().

The hello-paris bundle has the following external package dependency:

org.fusesource.example.time
hello-paris-impl

Fits the pattern of a provider bundle. The hello-paris-impl bundle implements the hello-paris API bundle.

The hello-paris-impl bundle has the following external package dependencies:

org.fusesource.example.hello.paris
org.fusesource.example.time
hello-boston

Fits the pattern of an API/provider combination bundle. The hello-boston bundle combines a Java interface and its implementation, where the Java interface returns a greeting, getGreeting(), and gives the local time in Boston, getLocalTime().

The hello-boston bundle has the following external package dependency:

org.fusesource.example.time
hello-consumer

Fits the pattern of a consumer bundle. The hello-consumer bundle imports the HelloParis OSGi service and the HelloBoston OSGi service and then invokes on these services to report the local times in Paris and in Boston.

The hello-consumer bundle has the following external package dependencies:

org.fusesource.example.hello.paris
org.fusesource.example.hello.boston
org.fusesource.example.time

The hello-consumer bundle effectively drives the sample application, obtaining references to the HelloBoston and HelloParis OSGi services, and then invoking methods on these services to obtain localised greetings and times.

The hello-consumer bundle contains the class, ConsumeHello, which is a client of the OSGi services, HelloBoston and HelloParis. To gain access to the OSGi services, ConsumeHello defines the setter methods, getHelloBoston() and getHelloParis(), and relies on the blueprint framework to inject the references. The entry point is the init() method, which gets invoked after the ConsumeHello bean is created and injected with the service references. The ConsumeHello class is defined as follows:

// Java
package org.fusesource.example.hello.consumer;

import org.fusesource.example.hello.boston.HelloBoston;
import org.fusesource.example.hello.paris.HelloParis;

public class ConsumeHello {
    protected HelloBoston helloBoston = null;
    protected HelloParis helloParis = null;

    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub

    }
    
    public void init() {
        if (helloBoston==null || helloParis==null) {
            System.out.println("Initialization failed. Injected objects are null.");
            return;
        }
        
        String enGreeting = helloBoston.getGreeting();
        String bostonTime = helloBoston.getLocalTime().getLocalTime();
        System.out.println("Boston says:" + enGreeting + " at " + bostonTime);
        
        String frGreeting = helloParis.getGreeting();
        String parisTime = helloParis.getLocalTime().getLocalTime();
        System.out.println("Paris says:" + frGreeting + " at " + parisTime);
    }

    public HelloBoston getHelloBoston() {
        return helloBoston;
    }

    public void setHelloBoston(HelloBoston helloBoston) {
        this.helloBoston = helloBoston;
    }

    public HelloParis getHelloParis() {
        return helloParis;
    }

    public void setHelloParis(HelloParis helloParis) {
        this.helloParis = helloParis;
    }
    
}

The basic principle of semantic versioning is that an increment to a major, minor, or micro part of a version signals a particular degree of compatibility or incompatibility with previous versions of a package. The Semantic Versioning technical white paper from the OSGi Alliance makes specific recommendations about how to interpret each of the major, minor, and micro parts of a version. Based on these definitions, the paper explains how to assign a version number to an exported package and how to assign a version range to an imported package.

This section summarizes the recommendations of the OSGi technical white paper.

To figure out how much version flexibility we can allow, we first need to consider the rules for backward compatibility in Java. Java is one of the few languages that explicitly considers issues of binary compatibility in its specification and the detailed rules can be found in the Binary Compatibility chapter of The Java Language Specification. The following is an incomplete list of changes that are binary compatibility with consumers of particular classes or interfaces:

  • Adding new fields, methods, or constructors to an existing class or interface.

  • Deleting private fields, methods, or constructors of a class.

  • Deleting package-only access fields, methods, or constructors of a class or interface.

  • Re-ordering the fields, methods, or constructors in an existing type declaration.

  • Moving a method upward in the class hierarchy.

  • Reordering the list of direct super-interfaces of a class or interface.

  • Inserting new class or interface types in the type hierarchy.

The fundamental idea of semantic versioning is that a bundle's version should indicate when the bundle breaks binary compatibility with its preceding version, bearing in mind that there are different kinds of binary compatibility: compatibility of consumers and compatibility of providers. The OSGi Semantic Versioning technical white paper proposes the following versioning conventions:

<major>

When a change breaks binary compabitility with both consumers and providers, increment the bundle's major version number. For example, a version change from 1.3 to 2.0 signals that the new bundle version is incompatible with older consumers and providers.

<minor>

When a change breaks binary compatibility with providers, but not consumers, increment the bundle's minor version number. For example, a version change from 1.3 to 1.4 signals that the new bundle version is incompatible with older providers, but compatible with older consumers.

<micro>

A change in micro version does not signal any backward compatibility issues. The micro version can be incremented for bug fixes that affect neither consumers nor providers of the API.

<qualifier>

The qualifier is typically used as a build identifier—for example, a time stamp.

Strictly speaking, importing and exporting works at the granularity level of packages, not of bundles. In principle, therefore, it is possible to assign versions at the level of individual packages, so that one bundle contains multiple packages with different versions. There are some scenarios where it can be useful to assign versions at package granularity.

For example, consider a bundle that contains both an API package and a package that implements the API (see API/Provider Build-Time Combination). In this case, it makes more sense to use separate versions for the API package and the implementation package.

Using the Maven bundle plug-in, you can specify the version of an individual Java package by creating or modifying the standard Java packageinfo file in the corresponding package directory. For example, if you want to assign version 1.2.1 to the org.fusesource.example.time package, create a file called packageinfo (no suffix) in the src/main/java/org/fusesource/example/time directory and add the following line:

version 1.2.1

Alternatively, since Java 5 it is also possible to specify version information using annotations in a package-info.java file, for example:

@Version("1.2.1")
package org.fusesource.example.time;

By default, the Maven bundle plug-in automatically assigns a version range to imported packages, following the semantic versioning rules outlined in Semantic Versioning. You need to provide an additional hint to the bundle plug-in, to indicate whether the bundle imports a package in the role of a consumer or a provider (the bundle plug-in presumes the consumer role).

For automatic import versioning to work, the package dependency must be versioned at build time, otherwise the importing bundle cannot calculate the import range.

The Maven bundle plug-in automatically generates consumer import ranges that conform to the rules of semantic versioning, as defined in Consumer import range. The only prerequisite is that the corresponding exporter actually defines a package version.

For example, given that the hello-paris bundle consumes version 1.2.1 of the org.fusesource.example.time package, the Maven bundle plug-in automatically generates a manifest with the following import:

Import-Package: org.fusesource.example.time;version="[1.0,2)"

When the API and the provider are to be combined in the same bundle, append the provide:=true clause to the relevant API packages listed in the Export-Package instruction (this is the correct setting to use both for an API/provider combination bundle and for an API/provider build-time combination bundle).

For example, given that the hello-boston bundle includes both the org.fusesource.example.hello.boston API and its implementation classes, you would define the Export-Package instructions for the hello-boston bundle as follows:

<instructions>
  ...
  <Export-Package>
    !${project.groupId}*.impl.*,
    !${project.groupId}*.internal.*,
	${project.groupId}.hello.boston*;provide:=true;version=${project.version}
  </Export-Package>
  ...
</instructions>

Given that the org.fusesource.example.hello.boston package has version 1.0, the Maven bundle plug-in generates a manifest with the following highlighted import and export:

[Note]Note

In the case where the API and the provider code are located in separate Maven projects, setting provide:=true on an exported API package in the provider's POM has the important side-effect that the API interfaces are included in the provider bundle. For a detailed explanation, see API/Provider Build-Time Combination.

Sometimes, a third-party exporter might follow a consistent versioning convention, but this convention is different from the OSGi convention. In this case, it make sense to define a custom import rule that codifies the alternative convention. For example, if a third-party exporter increments the minor version number whenever binary compatibility with consumers is broken, you could use Bnd's range macro to codify this rule on consumer imports, as follows:

<Import-Package><![CDATA[
  com.package.with.wrong.semantics*;version="$<range;[==,=+)>",
  *
  ]]>
</Import-Package>

Where the Bnd macro is written in the format, $<Macro> and must be enclosed in a CDATA section to avoid clashing with the XML interpretations of < and > (actually, Bnd supports a variety of different macro delimiters, most of which cannot be used here: braces, ${...}, clash with Maven properties; while square brackets, $[...], and parentheses, $(...), clash with the syntax of version ranges).

Entries like == and =+ are masks that return a version based on the version of the corresponding exporter. The equals sign, =, returns the corresponding version part unchanged; the plus sign, +, returns the corresponding version part plus one; and the minus sign, -, returns the corresponding version part minus one. For example, if the corresponding exporter has the version, 1.3.0.4, the range mask, [==,=+), resolves to [1.3,1.4). For more details, consult the Bnd documentation for the range macro.

The range macro is a relatively new addition to Bnd. In POMs that were written before the range macro became available, you might come across ranges written using the Bnd version macro. For example, the range, $<range;[==,=+)>, could also be written using the version macro as follows:

<Import-Package><![CDATA[
  com.package.with.wrong.semantics*;version="[$<version;==>,$<version;=+>)",
  *
  ]]>
</Import-Package>
[Note]Note

At the time of writing, the Bnd tool has a bug that causes two NullPointerException exceptions, accompanied by lengthy stack traces, to be generated whenever you build a Maven project featuring the range or version macros as shown above. Although alarming, these exceptions are harmless non-fatal errors. If you check the generated bundle after building, you will see that the Manifest.mf file is generated exactly as specified by the bundle instructions.