Friday, December 26, 2008

Beginner's Guide to OSGi on the Desktop

... a beginner's guide to OSGi on the desktop in which none of the following are discussed: history, politics, the spec, mobile devices, Sun, project JigSaw, any JSR, and certainly not any past spec leads (the poor souls).

Why should you care about OSGi? The screenshot below is a sample Swing app that evaluates Groovy script against any version of Groovy you have installed, despite that fact that these Groovy versions are not compatible and cannot typically be loaded side-by-side into the same JVM. The Groovy versions available in the UI are updated dynamically as new jars become available on your system with no restart of the frame necessary. The whole thing is 229 lines of code, 147 of which are Swing related.

OSGi gave me true sandboxed modules and dynamic service discovery in 82 lines of code and no XML. Wow.



If this doesn't excite you then your heart must be dead and you should seriously consider that MBA program you've been thinking about.

What's so cool about this?
These three versions of Groovy should not be allowed to coexist within the same JVM... they are incompatible. The Java classpath mechanism is a linear search order of a set of directories where classes are loaded. If two libraries use the same name then you have problems. The full set of all dependencies in your application typically needs to be compatible, but not so in an OSGi world. Dependencies can be privately held so that other modules don't see them.

Furthermore, OSGi makes it easy to detect available services and be notified when new services become available. You can handle these notifications any way you like, including loading the service without shutting your application down. So your web application can gracefully handle the rollout of a new "User Authentication Service" without a restart. And this example shows how to do all of this in 82 lines of code and zero XML.

How does OSGi work?
The whole system runs off of special class loaders and extended Jar manifest files. A module, or OSGi "bundle", is packaged as a standard Jar file, and may include other jar files within. The manifest of the jar declares what packages or classes it exports publicly and what packages or classes it uses internally. The specialized class loaders make sure that your module never sees any of the classes from other modules that aren't publicly declared. Thus, an app my reference Groovy 0.3, 1.0, and 1.6 all at the same time. Goodbye classpath, hello modules.

Service discovery is achieved by simply asking the container for registered bundles that declare to export a specific Java interface. Also, registering a listener with the container will notify your app when services come into (and go out of) existence. Your app no longer has a "public static void main". Instead, it has a start and stop method, and the container will call these when your app is started and stopped. Within start you may register yourself as a service or pull services out of the registry (or "context"). Within stop, you'll probably want to release any services you're using.

So what is this container? The OSGi container is simply a Java class you launch. My container jar is less than 1 megabyte in size and I launch it with the command:

java -jar org.eclipse.osgi-3.4.0.jar -console
It gives you a console in which you can install bundles (jar files, remember) by their URL. Once installed, the bundle can be started, stopped, refreshed, and uninstalled. As you can guess, having your bundle started causes your start method to be invoked; stopped and your stop method gets called. When a service is started, any service listeners are notified of the new service, and likewise when stopped. It can be done programmatically too, and couldn't be much easier.

How does this example work?
Running three different Groovy bundles within one UI required a total of 5 components: the user interface (Groovy Evaluator), the interfaces for the services (Groovy Provider), and the three service implementations (Groovy Provider Impl 0.3, 1.0, and 1.6).



The Groovy Provider exports itself as version 1.0. There is nothing in this jar file except a single POJO and the manifest specifying Export-Package: org.sample.provider;version="1.0.0".

The Groovy Provider Implementations are all the same except for the version numbers. They contain a simple POJO implementing the GroovyProvider interface, the associated Groovy jar file, a manifest, and an Activator. An Activator is an OSGi class, and contains the start and stop methods. Since this bundle is a service provider, it registers itself as a service, like so:
public class Activator implements BundleActivator {

ServiceRegistration registration;

public void start(BundleContext context) throws Exception {
GroovyProvider provider = new LegacyInterpreter();
registration = context.registerService(GroovyProvider.class.getName(), provider, null);
}

public void stop(BundleContext context) throws Exception {
registration.unregister();
}
}
* a groovy bundle actually needs to futz with the classloaders because of all the reflection it requires, but that's not important.

The manifest of the implementations need to import the GroovyProvider interface and also export the fact that they provide an implementation of that interface. Among other things, the manifest contains:
Export-Package: uses:="org.sample.provider";version="1.0.0"
Bundle-Activator: org.sample.Activator
Import-Package: org.sample.provider;version="1.0.0",org.osgi.framework;version="1.3.0"
Lastly, the Groovy Evaluator bundle needs to consume the services and register to listen for any new services that come online. The Activator for the evaluator can do something as simple as query the container for active services:
public class GroovyEvaluatorActivator implements BundleActivator {

public void start(BundleContext context) throws Exception {
String serviceName = GroovyProvider.class.getName();
ServiceReference[] references = context.getAllServiceReferences(serviceName, null);
for (ServiceReference ref : references) {
Object serviceHandle = context.getService(ref);
GroovyProvider service =(GroovyProvider)serviceHandle;
... do something with service
}
}
public void stop(BundleContext context) throws Exception {
}
}
But a more robust way is to subclass ServiceTracker and listen for service events. The code that does this is quite simple and can be found at: http://svn.assembla.com/svn/SampleCode/osgi/groovy-evaluator/src/main/java/org/sample/Activator.java. In this example, the JFrame simple adds and removes entries from it's JComboBox when new services are started and stopped.

Running the Application in the Container
I intentionally left this post light on full code listings. The entire source can be downloaded and built from subversion:
svn co http://svn.assembla.com/svn/SampleCode/osgi
Building the source requires Gradle (but not Groovy). Once downloaded, run:
gradle clean compile archive_jar
To run the OSGi console:
java -jar lib/osgi-3.4.0.jar -console
This opens the OSGi console where you can see what services are available. Within the console, install and start the bundles:
osgi> install file:/D:/dev/SampleCode/osgi/groovy-provider/build/groovy-provider-1.0.jar
Bundle id is 18
osgi> install file:/D:/dev/SampleCode/osgi/groovylib-0.3/build/groovy-provider-impl-0.3.jar
Bundle id is 19
osgi> install file:/D:/dev/SampleCode/osgi/groovylib-1.0/build/groovy-provider-impl-1.0.jar
Bundle id is 20
osgi> install file:/D:/dev/SampleCode/osgi/groovy-evaluator/build/groovy-evaluator-1.0.jar
Bundle id is 21
osgi> start 18 19 20 21
I intentionally did not start the Groovy 1.6 library, and the UI shows just the two Groovy versions installed. Now that the UI is open, you can install and start the last bundle and see how the UI updates:
osgi> install file:/D:/dev/SampleCode/osgi/groovylib-1.6/build/groovy-provider-impl-1.6.jar
Bundle id is 22
osgi> start 22
More information
This project's source code is a good place to start. Specifically, you can check to see exactly how the manifest files are created by looking at the build scripts (the several build.gradle files). There are a few other good resources as well. Sunil Patil wrote a 3 part writeup with full code listings on JavaWorld. Part 1 details the basics os a Hello World service. Part 2 describes the Spring DM product and adding Spring to OSGi. And Part 3 covers several web deployment scenarios and issues. Also, Joseph Ottinger has an OSGi for Beginners on The Server Side.

None of these guides gave me the full picture... I only started to grasp it all once I wrote a sample myself. All told, I spent about 12 to 16 hours with OSGi and I'm really happy I did. Java 7 isn't due until early 2010 and few shops will upgrade before late 2010. So the earliest you'll see JigSaw is 2011... 3 years from now.Why wait? Why not just click a few of the above links and get started now?

13 comments:

Josh Reed said...

Nicely done, Hamlet.

Saager Mhatre said...

After having tried to write an Eclipse RCP app, I'd stay away from this. The Activator class and related configuration just ticked me off after a while. Besides, you traded XML for Manifest files; doesn't eliminate externalized configuration.

Nevertheless, I guess most of what you're trying to do here is achievable using java.util.ServiceLoader with, if not less, pretty much the same amount of configuration. That's an age old pattern that I first saw in the JAXP libs and was finally released with Mustang.

Guillaume Laforge said...

Very interesting article, once again!

kiev gama said...
This comment has been removed by the author.
kiev gama said...

Nice work!
I've developed a really similar application a few months ago, which is available in google code:
http://code.google.com/p/scriptconsole4j/
I explain it a little here: http://ordinaryjava.blogspot.com/2008/04/scriptconsole4j-embed-scripting-console.html

But the idea is to use any of the available scripting languages, by selecting the language in the combo. I've adapted it to be used as an OSGi bundle providing access to the bundle context object enabling scripts that have access to the framework.

Josef said...

Beautiful! Now that's what I envisioned OSGi as enabling.

BTW, you couldn't resist and did mention Jigsaw at the end.

René Ghosh said...

Excellent little article. Loved that swipe about the "MBA program you've been thinking about."

tahir.akhtar said...

@Saager Mhatre

IMO, the main advantage when compared to ServiceLoaders is that OSGi frees you up from manually handling all ClassLoader glue code while giving you a nice high-level API.

Off-course you can leverage some heavy-weight containers to avoid manually dealing with class loaders but you will still have to do a lot of plumbing yourself.

Ubersoldat said...

Great explanation! Wish you wrote it 3 years ago when I started a SWT development :)

BTW, your blog schema is not very good for code stuff. The content column it's too short.

Saager Mhatre said...

@tahir.akhtar
I got that, but I think OSGi is too much baggage for the functionality it provides.

@all
As for the example Hamlet provides and scriptconsole4j, I think JSR 223 already provides for multiple language engines; trying to run multiple incompatible/conflicting implementations of the same language engine in the same JVM just seems to me an 'invented' problem.

But, to each their own. Well written article all the same. Sorry I came out a little strong in the last post; have bad experiences of Eclipse.

Hamlet D'Arcy said...

@all - On my current project we have Groovy 0.3 script deployed in the field. The rest of the project can't upgrade to anything newer because it would break existing deployed clients. In fact, the example screenshot showing the | as argument delimiter is the culprit. This is my real problem at my real job. I wish it weren't invented! We've worked around other language syntax issues from 0.3 using metaprogramming, but the final solution requires something like OSGi. For us at least.

Hamlet D'Arcy said...

For those interested in using Groovy and OSGi together... there is now a lengthy page on it in the Groovy Users Guide and sample code checked into the project. More info here: http://is.gd/go7S

言承旭Jerry said...
This comment has been removed by a blog administrator.