Using Bnd + Maven to Make Wrapping Complex Bundles Less Painful

Using Bnd + Maven to Make Wrapping Complex Bundles Less Painful


Taking standardized tests, getting wisdom teeth out, wrapping libraries as OSGi bundles at times we all must endure dinner pain. Whilst I can’t help with the anxiety of cramming for tests or the pain of getting wisdom teeth pulled, I can give you a leg up on wrapping your library as an OSGi bundle.

First though, what is an OSGi bundle and what do I mean by wrapping my library?

Wrapping Libraries in Bundles

Classloaders in OSGi work differently than classloaders in “vanilla” Java. Each bundle (essentially a Java Jar with a more complicated manifest) has it’s own classloader, allowing the bundle to operate with just it’s required dependencies and allowing multiple versions of the same package to be present in the OSGi container at one time.

To enable the container to generate these classloaders and understand the connection between the (potentially hundreds) of bundles in the container, each bundle contains a manifest including the packages that bundle imports and exports.

Wrapping a library as a bundle is the process of producing a bundle JAR with the correct manifest for a library that was not created to work with OSGi. bndtools has an excellent overview of the process for wrapping an external library.

This explanation works well for simple, single-tier dependencies, but what if the library you’re trying to wrap has it’s own non-OSGi-ified dependency tree?

A (less than) Hypothetical Scenario

Let’s just say that you were trying to incorporate a library created for a completely different class of Java application into OSGi such as Spring that was already widely adopted and tested. I’ve had to do this several times in my career.

Ideally, some or most of the dependencies for the library will already be available in the OSGi container or made available by another project like Apache ServiceMix, however especially if the library was never considered in the context of OSGi, you may have dependencies not already available as bundles. You’ll then find yourself in the “fun” situation of shoehorning in a library never meant to run in OSGi with dependencies which are not available in OSGi as the only alternative is to re-write the library.

You could of course track down every transient dependency and wrap that dependency, but you may not need that particular functionality elsewhere yet, and exposing that dependency as an OSGi bundle then means other bundles in the OSGi container can now import it thus potentially locking you into the current version.

As an alternative, you can instead include the dependencies in your wrapped bundle along with the original library you’re wrapping, something like this Maven snippet:

<plugin>
    <groupId>biz.aQute.bnd</groupId>
    <artifactId>bnd-maven-plugin</artifactId>
    <extensions>true</extensions>
    <version>6.2.0</version>
    <configuration>
        <bnd>
            <![CDATA[
            -includeresource: myco-commons-[0-9\\.]*.jar;lib:=true, \\
                javax.ws.rs-api-[0-9\\.]*.jar;lib:=true, \\
                jersey-(client|common)-[0-9\\.]*.jar;lib:=true
            Import-Package: *
        ]]>
        </bnd>
        <debug>true</debug>
    </configuration>
    <executions>
        <execution>
            <id>jar</id>
            <goals>
                <goal>jar</goal>
            </goals>
        </execution>
    </executions>
</plugin>

But how do you know what dependencies you need and what imports you can ignore? Especially when you have a multiple layer deep dependency tree?

You can, of course, keep installing the bundle and checking to see what packages fail to import, but this is both tedious and time-consuming. Instead, using Apache Maven and the bnd command line tool, you can dump all of the package imports for your project’s dependencies to text files for offline analysis:

# First download all of the dependencies to a folder
mkdir ./bundle-dependencies
mvn dependency:copy-dependencies -DoutputDirectory=./bundle-dependencies

# Then use BND to print the usage info
for j in ./bundle-dependencies/*.jar; do bnd print -u $j > $j.txt; done

# Finally, cleanup the JAR files
rm -f ./bundle-dependencies/*.jar

This will produce text files with every package used by every package in a particular library. For example this is the file generated for jersey-client 2.35:


[USES]

org.glassfish.jersey.client             javax.inject
                                        javax.net.ssl
                                        javax.ws.rs
                                        javax.ws.rs.client
                                        javax.ws.rs.core
                                        javax.ws.rs.ext
                                        org.glassfish.jersey
                                        org.glassfish.jersey.client.inject
                                        org.glassfish.jersey.client.internal
                                        org.glassfish.jersey.client.internal.inject
                                        org.glassfish.jersey.client.internal.routing
                                        org.glassfish.jersey.client.spi
                                        org.glassfish.jersey.internal
                                        org.glassfish.jersey.internal.guava
                                        org.glassfish.jersey.internal.inject
                                        org.glassfish.jersey.internal.spi
                                        org.glassfish.jersey.internal.util
                                        org.glassfish.jersey.internal.util.collection
                                        org.glassfish.jersey.message
                                        org.glassfish.jersey.message.internal
                                        org.glassfish.jersey.model.internal
                                        org.glassfish.jersey.process.internal
                                        org.glassfish.jersey.spi
                                        org.glassfish.jersey.uri
                                        org.glassfish.jersey.uri.internal
org.glassfish.jersey.client.authentication javax.annotation
                                        javax.ws.rs
                                        javax.ws.rs.client
                                        javax.ws.rs.core
                                        org.glassfish.jersey.client
                                        org.glassfish.jersey.client.internal
                                        org.glassfish.jersey.message
                                        org.glassfish.jersey.uri
org.glassfish.jersey.client.filter      javax.inject
                                        javax.ws.rs.client
                                        javax.ws.rs.core
                                        org.glassfish.jersey.client.internal
                                        org.glassfish.jersey.internal.inject
                                        org.glassfish.jersey.spi
org.glassfish.jersey.client.http        javax.ws.rs.core
                                        org.glassfish.jersey.client
org.glassfish.jersey.client.inject      org.glassfish.jersey.model
org.glassfish.jersey.client.internal    javax.net.ssl
                                        javax.ws.rs
                                        javax.ws.rs.client
                                        javax.ws.rs.core
                                        org.glassfish.jersey.client
                                        org.glassfish.jersey.client.spi
                                        org.glassfish.jersey.internal.l10n
                                        org.glassfish.jersey.internal.util
                                        org.glassfish.jersey.internal.util.collection
                                        org.glassfish.jersey.message.internal
org.glassfish.jersey.client.internal.inject javax.inject
                                        javax.ws.rs
                                        javax.ws.rs.ext
                                        org.glassfish.jersey.client
                                        org.glassfish.jersey.client.inject
                                        org.glassfish.jersey.client.internal
                                        org.glassfish.jersey.internal
                                        org.glassfish.jersey.internal.inject
                                        org.glassfish.jersey.internal.util
                                        org.glassfish.jersey.internal.util.collection
                                        org.glassfish.jersey.model
org.glassfish.jersey.client.internal.jdkconnector org.glassfish.jersey.internal.l10n
org.glassfish.jersey.client.internal.routing javax.ws.rs.core
                                        javax.ws.rs.ext
                                        org.glassfish.jersey.internal.routing
                                        org.glassfish.jersey.internal.util
                                        org.glassfish.jersey.message
                                        org.glassfish.jersey.message.internal
org.glassfish.jersey.client.spi         javax.net.ssl
                                        javax.ws.rs
                                        javax.ws.rs.client
                                        javax.ws.rs.core
                                        org.glassfish.jersey.client
                                        org.glassfish.jersey.process
                                        org.glassfish.jersey.spi

The package on the left is the package within that library and the packages on the right are the packages referenced by that package.

While you will still probably need to do the install and test cycle, this will make it significantly easier to understand where a dependency is referenced and if it can be excluded from the Import-Package statement.

While hopefully you won’t have to wrap a library like this as a bundle, I hope this post helps if you do!


← Demystifying Oak Search Part 2: Traversal 5 Maven Plugins to Turbocharge Your AEM Development →