Enterprise Java

Making Plain Old Java OSGi Compatible

Although OSGi is increasingly popular in the Java world, there are many Java applications and libraries that have not been designed to work in OSGi. Sometimes you may need to run such code inside an OSGi environment, either because you would like to take advantage of the benefits offered by OSGi itself, or because you need certain features only offered by this particular environment. Often, you can’t afford to migrate entirely to OSGi or at least you need a transition period during which your code works fine both in and outside OSGi. And surely, you would like to do this with minimum effort and without increasing the complexity of your software.

Recently, our team at SAP faced a similar challenge. We have a rather large legacy plain Java application which has grown over the years to include a number of homegrown frameworks and custom solutions. We needed to offer a REST-based interface for this application, and so we had either to include a Web server, or run inside an environment which has it. We opted for using SAP Lean Java Server (LJS), the engine behind SAP NetWeaver Cloud, which includes Tomcat and other useful services. However, LJS is based on Equinox, an OSGi implementation by Eclipse, and therefore we needed to make sure our code is compatible with OSGi to ensure smooth interoperability. In the process, we learned a lot about this topic and so I would like to share our most interesting findings with you in this post.

In order for plain Java code to run smoothly inside an OSGi environment, the following prerequisites should be met:

  • It is packaged as OSGi bundles, that is jar archives with valid OSGi manifests.
  • It adheres to requirements and restrictions imposed by OSGi regarding loading classes dynamically.
  • All its packages are exported by only one bundle, i.e. there are no different bundles exporting the same package.

Also, in many cases you may need to create a new entry point for your application to be started from the OSGi console. If you use Equinox, you should consider creating an Equinox application for this purpose.

Note that meeting the above requirements means neither that you should in any way migrate your code to OSGi, so that it only runs in OSGi, nor that you should fundamentally change your development environment or process to be based on OSGi. On the contrary, our experience shows that it is quite possible to achieve compatibility with OSGi without losing the capability to run outside OSGi, and without changing dramatically your development approaches and tools, by addressing the above requirements in the following ways:

  • All OSGi manifests can be generated automatically using BND and other tools based on it. Outside OSGi, these manifests are not used but also they don’t hurt.
  • Loading classes dynamically based on Class.forName() and custom classloading can be replaced by nearly identical mechanisms that use native OSGi services under the hood. It is possible to switch dynamically between the original and the OSGi mechanisms based on whether your code executes in OSGi with very little change to your existing code.
  • Alternatively, you could get rid of dynamic classloading in OSGi altogether by using the OSGi services mechanism for dynamic registration and discovery of “named” implementations.
  • Identical packages exported by more than one bundle should simply be renamed. Obviously this works outside OSGi as well.
  • Dependencies to OSGi can be minimized by placing all OSGi-specific code in a limited number of bundles, which preferably don’t contain code that should be executed outside OSGi as well.

The following sections provide more details regarding how this can be achieved.

Packaging as OSGi Bundles

In order to work inside an OSGi environment, all Java code should be packaged as OSGi bundles. This applies not only to all archives generated by your build, and to also to all their dependencies that are delivered as part of your software.

If your build uses Maven, you should strongly consider using Maven Bundle Plugin (which internally uses BND) to generate valid OSGi manifests for all archives produced by the build. In most cases, the manifests generated by the default configuration of this plugin will work just fine. However, in some cases certain minor tweaks and additions could be needed to generate the right manifests, for example:

  • Adding additional import packages for classes that are only used via reflection, and therefore could not be found by BND.
  • Specifying service component XMLs for bundles that expose OSGi declarative services.
  • Specifying bundle activators for bundles that depend on custom activation.

In our project, the bundle plugin is configured in our parent POM as follows:

<properties>
    <classpath></classpath>
    <import-package>*</import-package>
    <export-package>{local-packages}</export-package>
    <bundle-directives></bundle-directives>
    <bundle-activator></bundle-activator>
    <bundle-activationpolicy></bundle-activationpolicy>
    <require-bundle></require-bundle>
    <service-component></service-component>
    ...
</properties>
...
<build>
    <pluginManagement>
        <plugins>
            <plugin>
                <groupId>org.apache.felix</groupId>
                <artifactId>maven-bundle-plugin</artifactId>
                <version>2.3.4</version>
                <extensions>true</extensions>
                <configuration>
                    <encoding>${project.build.sourceEncoding}</encoding>
                    <archive> 
                        <forced>true</forced> 
                    </archive>
                    <instructions>
                        <Bundle-SymbolicName>${project.artifactId}${bundle-directives}</Bundle-SymbolicName>
                        <Bundle-Name>${project.artifactId}</Bundle-Name>
                        <_nouses>true</_nouses>
                        <Class-Path>${classpath}</Class-Path>
                        <Export-Package>${export-package}</Export-Package>
                        <Import-Package>${import-package}</Import-Package>
                        <Bundle-Activator>${bundle-activator}</Bundle-Activator>
                        <Bundle-ActivationPolicy>${bundle-activationpolicy}</Bundle-ActivationPolicy>
                        <Require-Bundle>${require-bundle}</Require-Bundle>
                        <Service-Component>${service-component}</Service-Component>
                    </instructions>
                </configuration>
                <executions>
                    <execution>
                        <id>bundle-manifest</id>
                        <phase>process-classes</phase>
                        <goals>
                            <goal>manifest</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            ...
        </plugins>
    </pluginManagement>
</build>

In the child POMs, we specify any of the properties that need to have a value different from the default. Such POMs are relatively few in our case.

Most of our dependencies also don’t have OSGi manifests, so we generate them as part of our build process. Currently, this is done by a Groovy script which uses the BND wrap command. For the majority of our dependencies, using a generic template for the manifest is sufficient. Currently, we use the following template, which is generated on the fly by the script:

Bundle-Name: ${artifactId}
Bundle-SymbolicName: ${artifactId}
Bundle-Version: ${version}
-nouses: true
Export-Package: com.sap.*;version=${version_space},*
Import-Package: com.sap.*;version="[${version_space},${version_space}]";resolution:=optional,*;resolution:=optional

I
n only a few cases, the manifest template has to contain information specific to the concrete jar. We captured such specifics by submitting these templates in our SCM and using the submitted version instead of the default.

Compliance with OSGi Classloading

Alternatives to Commonly Used Classloading Mechanisms

OSGi environments impose their own classloading mechanism which is described in more details in the following articles:

However, some plain Java applications and libraries often rely extensively on creating custom classloaders and loading classes via Class.forName() or ClassLoader.loadClass() in order to use reflection, and our application was one of them. This is problematic in OSGi as described in more details in OSGi Readiness – Loading Classes. The solutions proposed in this article, although valid, could not be directly applied in our case as this would involve changing heavily a large amount of legacy code, something that we didn’t want to do at this point.

We found that it is possible to solve this issue in an elegant way, transparently for the major bulk of our legacy code, relying entirely on native OSGi mechanisms. Instead of Class.forName(), one could use the following sequence of calls:

  • Use FrameworkUtil.getBundle() to get hold of the current Bundle and its BundleContext.
  • Get the standard PackageAdmin service from the OSGi service registry via the bundle context obtained in the previous step
  • Use PackageAdmin.getExportedPackage() and ExportedPackage.getExportingBundle() to find the Bundle which exports the package.
  • Finally, simply call Bundle.loadClass() to load the requested class.

In addition, although it is not possible to directly work with the low-level bundle classloader, the Bundle class itself provides classloading methods such as Bundle.loadClass() and Bundle.getResource(). Therefore, it is possible to create a custom classloader that wraps a bundle (or a number of bundles) and delegates to these methods.

To make the major bulk of your legacy code work in OSGi with only minor changes, it is sufficient to adapt it in the following way:

  • If the code executes in OSGi, instead of invoking Class.forName(), invoke a method which implements the sequence described above.
  • If the code executes in OSGi, instead of creating a custom classloader from a number of jar files, create a BundleClassLoader from the bundles corresponding to these jar files.

To make the above changes even more straightforward, in our application we introduced a new class named ClassHelper. It is a singleton which provides the following static helper methods that delegate to identical non-static methods of the single instance:

public static boolean isOsgi();
public static Object getBundleContext(Class&lt;?&gt; clazz);
public static Class&lt;?&gt; forName(String className, ClassLoader cl) throws ClassNotFoundException;
public static ClassLoader getBundleClassLoader(String[] bundleNames, ClassLoader cl);

The default implementation of these methods in the base ClassHelper class implement the default non-OSGi behavior – isOsgi() returns false, getBundleContext() and getBundleClassLoader() return null, and forName() simply delegates to Class.forName().

The class OsgiClassHelper inherits from ClassHelper and in turn implements the proper OSGi behavior described above. We put this class in its own special bundle to make sure that the bundle which contains ClassHelper and a large amount of other utilities is free from OSGi dependencies. This special bundle has an Activator, which replaces the default ClassHelper instance with an OsgiClassHelper instance upon bundle activation. Since the activation code is only executed in OSGi, this ensures that the proper implementation is loaded in both cases.

In the rest of our code, it was sufficient to simply replace invocations of Class.forName() with ClassHelper.forName(), and creation of custom classloaders with ClassHelper.getBundleClassLoader().

Using OSGi Services

In many plain Java applications, certain implementations are loaded based on a string “handle”, either the class name itself or something else. ClassLoader.loadClass(), often in combination with custom classloading, is commonly used for this purpose. OSGi offers the OSGi services mechanism for registration and discovery of such “named” implementations, which would allow you to get rid of dynamic classloading altogether. This mechanism is native to OSGi and offers a very elegant alternative to the custom mechanisms mentioned above. The downside of this approach, compared to the approach presented in the previous section, is that it requires somewhat deeper changes to your code, especially if it should continue to work outside OSGi as well.

You need to consider the following aspects:

  • Registering your interfaces and implementations in the OSGi service registry.
  • Discovering these implementations at runtime in the code that uses them.

Although you could register services programmatically, in most cases you would prefer using the OSGi declarative services approach, since it allows registering an existing implementation as an OSGi service in a purely declarative way. Regarding discovery, you could query the service registry directly via facilities provided by the BundleContext, or you could use the more powerful service tracker mechanism.
There are many excellent tutorials on OSGi services and declarative services in particular, among them:

In our case, we didn’t want to change our codebase too dramatically, so we switched to OSGi services in only a few places where we felt the positive impact would justify the investment. For the time being, we declared our existing implementations as services by adding service component XMLs. Although this XML-based approach is standard and commonly used, we find it rather verbose and inconvenient. An alternative approach would be to use annotations for specifying components and services, as described in the declarative services Wiki page and the OSGi Release 4 Draft Paper. These annotations are already supported by BND.

Additional Considerations

All Packages Exported by Only One Bundle

Exporting the same package from more than one bundle doesn’t work well in OSGi and so has to be avoided. If you have such cases in your code, you should rename these packages appropriately.

Exposing an OSGi Entry Point

Finally, you may need to provide a new entry point for starting your application from the OSGi console. If you use Equinox, one appropriate mechanism for this is creating an Equinox application, which involves implementing the org.eclipse.equinox.app.IApplication interface and providing one additional plugin.xml, as described in Getting started with Eclipse plug-ins: command-line applications. This application can be started from the Equinox OSGi console using the startApp command.

Conclusion

It is possible to make plain Java applications and libraries OSGi compatible with relatively little effort and manageable impact on your existing code, by following the guidelines and approaches described in this post.

Do you have similar experience with making Java code compatible with OSGi? If yes, I would love to hear about it.
 

Reference: Making Plain Old Java OSGi Compatible from our JCG partner Stoyan Rachev at the Stoyan Rachev’s Blog blog.

Stoyan Rachev

Software developer, architect, and agile software engineering coach at SAP
Subscribe
Notify of
guest

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

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button