Modularity is an important aspect of large Java systems. Build scripts and projects are often split up into modules in order to improve the build, but this is rarely taken into account at runtime.
In the second of the Modular Java series, we'll cover static modularity. We'll describe how to create bundles, how to install them into an OSGi engine and how to set up (versioned) dependencies between bundles. In the next episode, we'll look at dynamic modularity and show how bundles can react to other bundles coming and going.
As covered last time in Modular Java: What Is It?, Java has always had packages as a unit of modularity at development time, and JARs as a unit of modularity at deployment time. However, although build tools like Maven guarantee a certain combination of packages and JARs at compile time, these dependencies may be inconsistent with the runtime classpath. To solve this problem, modules can declare their dependency requirements so that, at runtime, they can be checked prior to execution.
OSGi is a runtime dynamic module system for Java. Specifications describe the behaviour of how the OSGi runtime works; the current release is OSGi R4.2 (as covered by InfoQ previously).
An OSGi module (also known as a bundle) is just a simple JAR file, with additional information in the archive's MANIFEST.MF
. At a minimum, each bundle's manifest must contain:
- Bundle-ManifestVersion: must be 2 for OSGi R4 bundles (otherwise defaults to 1 for OSGi R3 bundles)
- Bundle-SymbolicName: textual identifier of the bundle, often in reverse domain name format such as
com.infoq
and frequently corresponds to the packages contained therein - Bundle-Version: a version of the form major.minor.micro.qualifier where the first three elements are numeric (defaulting to 0) and qualifier is textual (defaulting to the empty string)
Creating a bundle
The simplest bundle contains purely a manifest file as follows:
Bundle-ManifestVersion: 2 Bundle-SymbolicName: com.infoq.minimal Bundle-Version: 1.0.0
That wouldn't be tremendously exciting to create, however, so let's create one with an activator. This is an OSGi-specific piece of code which is invoked when the bundle is started, like a per-bundle main method.
package com.infoq; import org.osgi.framework.*; public class ExampleActivator implements BundleActivator { public void start(BundleContext context) { System.out.println("Started"); } public void stop(BundleContext context) { System.out.println("Stopped"); } }
In order for OSGi to know which class is the activator, we need to add two extra items to the manifest:
Bundle-Activator: com.infoq.ExampleActivator Import-Package: org.osgi.framework
Bundle-Activator
declares the class to instantiate and invoke the start()
method when the bundle is started; similarly, the stop()
method is called when the bundle is stopped.
What about the Import-Package
? Each bundle needs to define its dependencies in the manifest, in order to determine at runtime whether all the required code is available. In this case, the ExampleActivator
depends on BundleContext
from the org.osgi.framework
package; if we don't declare that dependency in the manifest, we'll get a NoClassDefFoundError
at runtime.
Downloading an OSGi engine
To compile and test our bundle, we'll need an OSGi engine. There are several open-source engines available for OSGi R4.2, listed below. It is also possible to download the Reference API to compile against (which guards against using any platform-specific features); however, you still need an OSGi engine to run it. Here are the options:
Equinox | |
---|---|
License | Eclipse Public License |
Documentation | http://www.eclipse.org/equinox/ |
Download | org.eclipse.osgi_3.5.0.v20090520.jar |
Notes |
The To get a console, supply |
Framework | org.eclipse.osgi_3.5.0.v20090520.jar |
Felix | |
License | Apache License |
Documentation | http://felix.apache.org/ |
Download | Felix Framework Distribution 2.0.0 |
Notes | Seen as the most strictly compliant of the OSGi engines, it's also used in GlassFish and many other open-source products. You need to run java -jar bin/felix.jar instead of java -jar felix.jar since it looks for a path bundles to auto start from the current directory. |
Framework | bin/felix.jar |
Knopflerfish | |
License | Knopflerfish License (BSD-esque) |
Documentation | http://www.knopflerfish.org/ |
Download | knopflerfish_fullbin_osgi_2.3.3.jar |
Notes | This JAR is a self-extracting zip file; you have to run with java -jar initially to unpack. Don't download the 'bin_osgi ' alternative as this fails to start. |
Framework | knopflerfish.org/osgi/framework.jar |
Whilst there are also smaller OSGi R3 runtimes available (like Concierge) that target embedded devices, this series will focus on OSGi R4.
Compiling and running a bundle
Having got your framework.jar
, compiling the example above is a case of adding the OSGi framework to the classpath, and then packaging it up into a JAR:
javac -cp framework.jar com/infoq/*/*.java
jar cfm example.jar MANIFEST.MF com
Each of the engines has some kind of shell with similar (but not identical) commands. For the purposes of this exercise, we'll just look at how to get the engine up and running, and install and start/stop the bundle.
Once the engine is up and running, you can install the bundle (specified as a file://
URL), and then using the returned numeric bundle id, start and stop it.
Equinox | Felix | Knopflerfish | |
---|---|---|---|
Launch | java -jar org.eclipse.osgi_*.jar -console |
java -jar bin/felix.jar |
java -jar framework.jar -xargs minimal.xargs |
Help | help |
||
List | ss |
ps |
bundles |
Install | install file:///path/to/example.jar |
||
Start | start id |
||
Update | update id |
||
Stop | stop id |
||
Uninstall | uninstall id |
||
Shutdown | exit |
shutdown |
Although all shells work in roughly the same way, the subtle differences between commands can be slightly confusing. A couple of harmonised console projects (Pax Shell, Posh) and launchers (Pax Runner) are available; OSGi RFC 132 is an ongoing proposal to try and standardise the command shells. Apache Karaf, which aims to be a distribution that can run on top of Equinox or Felix and provides a harmonised shell as well as other features. Whilst using these for real deployments might be advisable, this series will focus on the vanilla OSGi framework implementations.
If you launch your OSGi framework, you should be able to install the com.infoq.minimal-1.0.0.jar
from above (you can also install this straight from the site using the link's address and the install
command). Each time you install
the bundle, it will print out the numeric ID of that bundle.
Depending on what other bundles are in the system, it's impossible to know what bundle identifier you'll end up with when installing; but you should be able to list the installed bundles using the appropriate command to find out.
Dependencies
So far, we've just got a single bundle. One of the benefits of modularity is that we can decompose our system into a number of smaller modules, and in doing so, reduce the application's complexity. To some extent, this is already done with Java's packages – for example, a common
package with a client
and server
packages is often used in networked applications. The unstated implication is that the client
and server
packages are independent, and both depend on the common
package; but as with Jetty's recent example (where client
accidentally ended up depending on server
) this isn't always easy to achieve. In fact, some of the successes that OSGi brings to a project is purely the enforced modularity constraints between modules.
Another benefit to modularisation is to separate out the 'public' packages from the non-public ones. Java's compile-time system allows for hidden non-public classes (those visible within a specific package) but does not provide a greater degree of flexibility than that. However, in an OSGi module, you can choose which packages are exported with the implicit fact that non-exported packages are not visible to other modules.
Let's say we're going to develop a function to instantiate URI Templates (as used in Restlet). Since this is likely to be reusable, we want to put this in its own module and have clients who need to use it depend on us. (Normally, bundles aren't quite as fine-grained as this; but it demonstrates the principle.) The function will take a template, like http://www.amazon.{tld}/dp/{isbn}/
, and with a Map
containing tld=com,isbn=1411609255
, we can generate the URL http://www.amazon.com/dp/1411609255/
(One of the reasons to do this is to allow us to change the template if Amazon's URL scheme changes, although Cool URIs don't change.)
In order to provide an easy way to switch between different implementations, we'll provide an interface and a factory. This will also allow us to see how we can hide the implementation away from clients whilst still giving them the functionality. The code (across several source files) will look like:
package com.infoq.templater.api; import java.util.*; public interface ITemplater { public String template(String uri, Map data); } // --- package com.infoq.templater.api; import com.infoq.templater.internal.*; public class TemplaterFactory { public static ITemplater getTemplater() { return new Templater(); } } // --- package com.infoq.templater.internal; import com.infoq.templater.api.*; import java.util.*; public class Templater implements ITemplater { public String template(String uri, Map data) { String[] elements = uri.split("\\{|\\}"); StringBuffer buf = new StringBuffer(); for(int i=0;i<elements.length;i++) buf.append(i%2 == 0 ? elements[i] : data.get(elements[i])); return buf.toString(); } }
The implementation is hidden away in the com.infoq.templater.internal
package, whilst the public API is in the com.infoq.templater.api
package. This will give us the flexibility to change the implementation to a more efficient mechanism later on, if needed. (The internal
package name is somewhat conventional, but you can call it what you want.)
To give other bundles access to the public API, we need to export it from the bundle. Our manifest will look like:
Bundle-ManifestVersion: 2 Bundle-SymbolicName: com.infoq.templater Bundle-Version: 1.0.0 Export-Package: com.infoq.templater.api
Creating a client bundle
We can now create a client that uses the templater. Using the example above, create an activator whose start()
method looks like:
package com.infoq.amazon; import org.osgi.framework.*; import com.infoq.templater.api.*; import java.util.*; public class Client implements BundleActivator { public void start(BundleContext context) { Map data = new HashMap(); data.put("tld", "co.uk"); // or "com" or "de" or ... data.put("isbn", "1411609255"); // or "1586033115" or ... System.out.println( "Starting\n" + TemplaterFactory.getTemplater().template( "http://www.amazon.{tld}/dp/{isbn}/", data)); } public void stop(BundleContext context) { } }
We'll need to define in our manifest that we're explicitly importing the templater API, as otherwise our bundle won't compile. We can either specify the dependency using either Import-Package
or Require-Bundle
. The former allows us to import packages individually; the latter will implicitly import all exported packages from the bundle. (Multiple packages and bundles are comma separated.)
Bundle-ManifestVersion: 2 Bundle-SymbolicName: com.infoq.amazon Bundle-Version: 1.0.0 Bundle-Activator: com.infoq.amazon.Client Import-Package: org.osgi.framework Require-Bundle: com.infoq.templater
Note that in the earlier example, we were already using the Import-Package
to import org.osgi.framework
. In this case, we are demonstrating the use of Require-Bundle
which uses the Bundle-SymbolicName
. We could have equally well added it in as Import-Package: org.osgi.framework, com.infoq.templater.api
instead.
Regardless of how we declare our dependency on the templater
bundle, we only get access to the single exported package com.infoq.templater
. Although the client can access the templater via TemplaterFactory.getTemplater()
, we can't directly access the class from the internal
package. This gives us the flexibility to change implementation in the future without breaking clients.
Testing the system
Any OSGi application is really just a set of bundles. In this case, we need to compile and JAR up the bundles (as previously), start up an OSGi engine, and install the two bundles. Here's what it would look like in Equinox:
java -jar org.eclipse.osgi_* -console osgi> install file:///tmp/com.infoq.templater-1.0.0.jar Bundle id is 1 osgi> install file:///tmp/com.infoq.amazon-1.0.0.jar Bundle id is 2 osgi> start 2 Starting http://www.amazon.co.uk/dp/1411609255
The Amazon client bundle has started; and when it did, it instantiated the URI template for us with the (admittedly hard-coded) values previously. It then prints out during the startup of the bundle itself to confirm that it worked. Clearly, a real system wouldn't be so inflexible; but the Templater service could be used in any other application (for example, generating links in a web-based application). We'll look at web applications in an OSGi context in the future.
Versioned dependencies
The final point for this instalment is to note that the dependencies that we have at the moment are unversioned; or rather, that we'll be able to use any version. Both bundles as a whole and packages individually can be versioned, and increases in the minor number are used to signify new features (but remaining backwardly compatible). The org.osgi.framework
package, for example, was version 1.4.0 in OSGi R4.1 and 1.5.0 in OSGi R4.2. (This, incidentally, is a good reason to keep the bundle version and marketing version as separate concepts, something that the Scala language has yet to learn.)
To declare a dependency on a specific version, we have to express that in the Import-Package
or Require-Bundle
. For example, we can specify Require-Bundle: com.infoq.templater;bundle-version="1.0.0"
to signify that we want a minimum version of 1.0.0 in order to work. Similarly, we could do the same with Import-Package: com.infoq.templater.api;version="1.0.0"
– but bear in mind that the version of the package is distinct from the version of the bundle. If you don't specify a version, it defaults to 0.0.0, so unless you have a corresponding Export-Package: com.infoq.templater.api;version="1.0.0"
then this import won't be resolved.
It's also possible to specify a version range. For example, the conventional meaning to OSGi version numbers is that an increment of the major number indicates a change in backward compatibility, so we might just want to restrict ourselves to the 1.x range. To do that, we can express a (bundle-)version="[1.0,2.0)"
dependency constraint. In this case, the [ means 'inclusive' and ) means 'exclusive', or in other words 'from 1.0 up to but not including 2.0 onwards'. In fact, expressing a dependency constraint of "1.0" is the same as "[1.0,∞)" – in other words, anything bigger than 1.0.
Although it's outside of the scope of this article, it's possible to have multiple versions of a bundle in an OSGi system at once. This might be useful if you have an old client which depends on version 1.0 of your API, as well as having a new client which depends on version 2.0 of your API. As long as the dependencies for each bundle are consistent (in other words, you don't have a bundle which tries to import 1.0 and 2.0 at the same time, directly or indirectly) then the application will run fine. As an exercise for the reader, you can create a version 2.0 of the Templater API to use generics, and then have a client which only depends on version 1.x as well as one that uses version 2.x.
Summary
In this article, we've explored the open-source OSGi engines Equinox, Felix and Knopflerfish, and created a dependent pair of bundles. We've also touched upon versioned dependencies. At the moment, this modularity is static; we haven't explored any of the dynamic nature of OSGi. We'll be looking at that in the next instalment.
Installable bundles (also containing source):