Enterprise Java

Understanding how OSGI bundles get resolved

I’d like to review how OSGI bundles get resolved and use Apache Karaf to demonstrate. Karaf is a full-featured OSGI container based on the Apache Felix kernel and is the corner stone for the Apache ServiceMix integration container. For part one, I will discuss how bundles are resolved by an OSGI framework. In part two, I’ll demonstrate each rule using Apache Karaf. Let’s get started.

Bundle Resolution Rules

An OSGI bundle’s lifecycle defines the possible states and transitions for a bundle. We will be discussing the “Resolved” state of a bundle which means the state it can reach after being “Installed” and when all of its required dependencies are satisfied. Traditional Java classloading is susceptible to runtime ClassCastExceptions where two classes with the same fully-qualified name from two different class loaders become mixed up and one is used in the wrong classpath space. One of the main goals of OSGI is to avoiding this kind of runtime exception by resolving all dependencies at deploy time with the idea being failing “fast” at deploy time will be easier to debug than trying to track down classloading issues at runtime. Think about how annoying some of the class not found or class cast exceptions are to debug in a Weblogic deployment, for example. OSGI solves this. For a bundle to reach the “Resolved” state, it must have it’s dependencies fulfilled. Think of the “fail fast” approach to bundle resolution like this: if you use a spring application, and one of your beans cannot be wired properly because a bean definition is missing, you will know this at deploy time instead of when a customer is calling your code. The same principle is applied with OSGI; instead of object-level wiring dependencies, we are wiring module and class-loading dependencies.
A trivial explanation of a bundle having its dependencies resolved could go like this: if a bundle imports (Import-Package) a specific package, that package must be made available by another bundle’s exports (Export-Package). If bundle A has Import-Package: org.apache.foo then there must be a bundle deployed that has an Export-Package: org.apache.foo

For every Import-Package package declaration, there must be a corresponding Export-Package with the same package

Bundles can also attach other attributes to the packages it imports or exports. What if we added a version attribute to our example:

Bundle-Name: Bundle A
Import-Package: org.apache.foo;version="1.2.0"

This means, Bundle A has a dependency on package org.apache.foo with a minimum version of 1.2.0. Yes, you read correctly. Although with OSGI you can specify a range of versions, if you don’t specify a range but rather use a fixed version, it will result in a meaning of “a minimum” of the fixed value. If there is a higher version for that same package, the higher version will be used. So bundle A will not resolve correctly unless there is a corresponding bundle B that exports the required package:

Bundle-Name: Bundle B
Export-Package: org.apache.foo;version="1.2.0"

Note that the reverse is not true… If Bundle B exported version 1.2.0, Bundle A is not required to specify a version 1.2.0. It can use this import and resolve just fine:

Bundle-Name: Bundle A
Import-Package: org.apache.foo

This is because imports declare the versions they need. An exported version does not specify anything an importing bundle must use (which holds for any attributes, not just version).

Import-Package dictates exactly what version (or attribute) it needs, and a corresponding Export-Package with the same attribute must exist

What happens if you have a scenario where Bundle A imports a package and it specifies a version that is provided by two bundles:

Bundle-Name: Bundle A
Import-Package: org.apache.foo;version="1.2.0"

Bundle-Name: Bundle B
Export-Package: org.apache.foo;version="1.2.0"

Bundle-Name: Bundle C
Export-Package: org.apache.foo;version="1.2.0"

Which one bundle does Bundle A use?
The answer is it depends on which bundle (B or C) was installed first.

Bundles installed first are used to satisfy a dependency when multiple packages with the same version are found

Things can get a little more complicated when hot deploying bundles after some have already been resolved. What if you install Bundle B first, then try to install Bundle A and the following Bundle D together:

Bundle-Name: Bundle D
Export-Package: org.apache.foo;version="1.3.0"

As we saw from above, the version declaration in Bundle A (1.2.0) means a minimum version of 1.2.0; so if a higher version was available then it would select that (version 1.3.0 from Bundle D in this case). However, that brings us to another temporal rule for the bundle resolution:

Bundles that have already been resolved have a higher precedence that those not resolved

The reason for this is the OSGI framework tends to favor reusability for a given bundle. If it’s resolved, and new bundles need it, then it won’t try to have many other versions of the same package if it doesn’t need to.

Bundle “uses” directive

The above rules for bundle resolution are still not enough and the wrong class could still be used at runtime resulting in a class-cast exception or similar. Can you see what could be missing?
What if we had this scenario. Bundle A exports a package, org.apache.foo, that contains a class, FooClass. FooClass has a method that returns an object of type BarClass, but BarClass is not defined in the bundle’s class space, it’s imported like this:
public class FooClass {
    public BarClass execute(){ ... }
}

Bundle-Name: Bundle A
Import-Package: org.apache.bar;version="3.6.0"
Export-Package: org.apache.foo;version="1.2.0"

So far everything is fine as long as there is another bundle that properly exports org.apache.bar with the correct version.

Bundle-Name: Bundle B
Export-Package: org.apache.bar;version="3.6.0"

These two bundles will resolve fine. Now, if we install two more bundles, Bundle C and Bundle D that look like this:

Bundle-Name: Bundle C
Import-Package: org.apache.foo;version="1.2.0", org.apache.bar;version="4.0.0"

Bundle-Name: Bundle D
Export-Package: org.apache.bar;version="4.0.0"

We can see that Bundle C imports a package, org.apache.foo from Bundle A. Bundle C can try to use FooClass from org.apache.foo, but when it gets the return value, a type of BarClass, what will happen? Bundle A expects to use version 3.6.0 of BarClass, but bundle C is using version 4.0.0. So the classes used are not consistent within bundles at runtime (i.e., you could experience some type of mismatch or class cast exception), but everything will still resolve just fine at deploy time following the rules from above. What we need is to tell anyone that imports org.apache.foo that we use classes from a specific version of org.apache.bar, and if you want to use org.apache.foo you must use the same version that we import. That’s exactly what the uses directive does. Let’s change bundle A to specify exactly that:

Bundle-Name: Bundle A
Import-Package: org.apache.bar;version="3.6.0"
Export-Package: org.apache.foo;version="1.2.0"";uses:=org.apache.bar

Given the new configuration for Bundle A, the bundles would not resolve correctly from above. Bundle C could not resolve, because it imports org.apache.foo but the “uses” constraint on Bundle A specifies that C must use the same version that A does (3.6.0) for org.apache.bar, otherwise the bundle will not resolve when trying to deploy. The solution to this is change the version in Bundle C for org.apache.bar to be 3.6.0.

Using the Apache Karaf

OSGI container Karaf is based on the Apache Felix core, although the Equinox core can be substituted if desired. Karaf is a full-featured OSGI container and is the cornerstone of the Apache ServiceMix integration container. ServiceMix is basically Karaf but specifically tuned for Apache Camel, Apache ActiveMQ and Apache CXF.
This tutorial will require Maven and Karaf. Download maven from the maven website. Download and install karaf as described in the getting started guide on the Karaf website. You will also need the code that goes along with this example. You can get it at my github repo. After getting it, make sure to run ‘mvn install’ from the top-level project. This will build and install all of the bundles into your local maven repository. Although you can install bundles a couple different ways, using maven is easiest. Note that this sample code is mostly made up of package names without any real Java classes (except where the tutorial specifies).

First thing to do is start up karaf. In a plain distribution there should be no bundles installed. Verify this by doing “osgi:list” at the karaf commandline. Going in order, we will test out the rules given above.

For every Import-Package package declaration, there must be a corresponding Export-Package with the same package

To test this rule, let’s install Bundle A from our sample bundles. Bundle A specifies an Import-Package of “org.apache.foo” package. According to the first rule, this bundle cannot move to the “Resolved” state since there is no corresponding bundle with an “Export-Package” of org.apache.foo.
From the karaf commandline, type “osgi:install mvn:explore-bundle-resolution/bundleA/1.0?. This will install the bundleA bundle. Now do a “osgi:list” again. You should see the bundle installed, and under the “State” column, it should say “Installed”. Now try “osgi:resolve bundle id” where bundle id is the ID listed from the “osgi:list” command. This will try to resolve all bundle dependencies and put it into the “Resolved” state. It won’t resolve, however. Type “osgi:list” again to see the state of the bundle. It’s still in “Installed” state even though we asked OSGI to resolve it. Let’s find out why. Execute the “osgi:headers bundle id“. Under the Import-Package, you should see the package name org.apache.foo listed in a red color. This dependency is missing, so let’s add it. Type “osgi:install -s mvn:explore-bundle-resolution/bundleB/1.0?. Note the ‘-s’ switch in the command. This tells OSGI to start the bundle once it’s installed. Now type the osgi:resolve command again (with the appropriate bundle ID). This will now resolve the bundle. 

Import-Package dictates exactly what version (or attribute) it needs, and a corresponding Export-Package with the same attribute must exist

Let’s install bundle C: “osgi:install -s mvn:explore-bundle-resolution/bundleC/1.0? List the bundles again, and you’ll see that although bundle C depends on org.apache.foo, it specifies an Import-Package with a specific version=1.5. There is no version 1.5 that is resolved, so bundle C will also not resolve. Bundle D happens to export a package org.apache.foo with a version equal to 1.5. Install bundle D the same way we’ve installed the others, using the -s to start it. Now try to resolve bundle C and it should work (“osgi:resolve bundle id“).

Bundles installed first are used to satisfy a dependency when multiple packages with the same version are found

This rule says that if there are multiple packages exported with the same version, OSGI will choose the first-installed bundle to use when trying to resolve bundles that import the package. Continuing on with the previous example where we installed bundle C and D… consider that bundle D exports org.apache.foo;version=1.5. So if we install bundle F that exports the exact same package and version, we should see that bundle C is resolved with the package from bundle D and not bundle F. Let’s see.. install bundle F: “osgi:install -s mvn:explore-bundle-resolution/bundleF/1.0?. Do an osgi:list and see that both bundle D and F are correctly installed and “Active”. This is a cool feature of OSGI: we can have multiple versions of the same package deployed at the same time, (including in this example the exact same version). Now we should uninstall bundle C and re-install it to see which bundle it uses to resolve for its import of org.apache.foo. Try running “osgi:uninstall bundle id” to uninstall bundle C. Now re-install it using the command from above. It should resolve to use bundle D. Use “package:import bundle id” to verify. You can try switching things around to get F to resolve. You may need to use “osgi:refresh” to refresh the OSGI bundles. 

Bundles that have already been resolved have a higher precedence that those not resolved

In a way, we have already seen this with the previous rule, but this rule comes into play when hot deploying. This is left as an exercise to the reader as this post is already getting pretty long and I would like to cover the “uses” directive next. 

Bundle “uses” directive

The “uses” directive adds one of the last rules and constraints to avoid runtime class-cast exceptions. To simulate how the “uses” directive works, we will install bundles G, H, I, and J and notice how the container enforces the “uses” directive.
Bundle G represents a sort of “service” module that client modules can call to “execute” some form of processing and return a result. The result it returns is an object of type BarClass that comes from Bundle H. But if a client makes a call to bundle G, it too must use the BarClass from bundle H or it will result in a class cast exception. In our samples, Bundle I is the client code and Bundle J represents a different version of the BarClass. Install the bundles in any order you like, but my demonstration followed this order: J, H, G, I. Note that the version of org.apache.bar is indeed the 2.0.0 version which comes from bundle H even though bundle H was installed second (contrary to the rule above). This is because bundle G specified the “uses” directive to depend on a specific version of org.apache.bar.

Reference: Understanding how OSGI bundles get resolved from our JCG partner Christian Posta at the Christian Posta Software blog.

Christian Posta

Christian is a Principal Consultant at FuseSource specializing in developing enterprise software applications with an emphasis on software integration and messaging. His strengths include helping clients build software using industry best practices, Test Driven Design, ActiveMQ, Apache Camel, ServiceMix, Spring Framework, and most importantly, modeling complex domains so that they can be realized in software. He works primarily using Java and its many frameworks, but his favorite programming language is Python. He's in the midst of learning Scala and hopes to contribute to the Apache Apollo project.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button