Wednesday, May 6, 2009

OSGi With Scala, Java, Groovy, Maven and PAX

In this first installment, I'm going to attempt to walk you through the setup of an OSGi project that is comprised of three OSGi bundles; one for Java, one for Groovy and one for Scala. In an effort to keep it simple, I'm going to employ a trivial hello world example in each bundle. The emphasis of this post is to introduce some concepts and illustrate how to get an environment up and running. I'm going to leverage maven and the extremely useful PAX maven plugin to automate much of the grunt setup work, packaging and runtime management. In subsequent posts, I'll show how we can leverage Spring DM to eliminate the OSGi API from our code and I'll show how to get these bundles to actually interact with each other.

I think that OSGi has an enormous amount of potential. There are many high profile commercial and open source projects that make exceptional use of the framework already. The buzz is finally catching up and support for OSGi from a tooling and bundle availability standpoint continue to improve. I think Patrick Paulin's recent post addressing the question Why is OSGi Important sums things up nicely.

These technologies are all advancing at a pretty rapid pace, so in an attempt to future-proof this post I'd like to take a quick moment to identify precisely what versions I'm using for these toolsets. From a maven perspective, I'm using version 2.0.10 of maven itself, version 1.0-rc5 of the gmaven-plugin and the gmaven-runtime-1.6, version 2.9.1 of the maven-scala-plugin and version 1.4 of the maven-pax-plugin. I'm also using java version 1.5.0_16 on Mac OS X with Groovy 1.6.2 and Scala 2.7.4.

If you'd prefer to look at the code as you go, you can grab it from bitbucket. Be sure to update your working copy to revision 0 to see the code as it would look at the completion of this first article.

        hg clone http://bitbucket.org/brimurph/helloworld

        cd helloworld

        hg update -r 0
    

Java

OK. enough with the words. Let's get into some code. As I mentioned, we're going to leverage PAX to automate a lot of the boilerplate maven setup work. Please be sure that you've got maven installed and that it's in your $PATH. The first step is to go ahead and create our project.

        mvn org.ops4j:maven-pax-plugin:create-project -DgroupId=com.domain.osgi -DartifactId=helloworld -Dversion=1.0-SNAPSHOT
    

I'd encourage you at this point to peruse everything that pax generated. There's a parent pom.xml that outlines the settings for the maven-pax-plugn. It also defines maven modules named poms and provision. The poms module goes on to define two more modules for wrappers and compiled. I have a love / hate relationship with maven. There's an enourmous amount of flexibility provided, but it comes at a cost of complexity. I could devote entire articles detailing the ins and outs of all the configuration options provided by the various pom.xml files created. Fortunately, as you'll see throughout this article, PAX will do most of the dirty work for us.

An OSGi project generated by PAX may contain 1 or more bundles. Each bundle we create simply gets registered as another independent maven module. To get started, cd into the helloworld project directory that PAX created and issue the following command to create a bundle:

        mvn org.ops4j:maven-pax-plugin:create-bundle -Dpackage=com.domain.osgi.java -DbundleName=hello-java
    

That's it. We now have a fully functional Java OSGi bundle. It can be compiled and run as is. Feel free to peruse what was generated and see what the ExampleService is all about before proceeding.

For our purposes, I'd like to remove the generated code so we can start anew. cd into the helloworld/hello-java bundle directory that PAX created and let's tidy up and then write some Java code.

        rm -rf src/main/java;
        
        mkdir -p src/main/java/com/domain/osgi/java/internal;
        
        cd src/main/java/com/domain/osgi/java;
    

First up, we'll create an interface named HelloJavaService.java. This interface will be registered and exposed in our OSGi BundleContext.

        package com.domain.osgi.java;
        
        public interface HelloJavaService {
            void hello();
        }
    

Again, this is a trivial example. All we're doing is defining a hello() method with no return value. Notice that there's nothing about this interface that is specific to OSGi. Next we'll cd into the internal package and create an implementation of our service named HelloJavaServiceImpl.java

        package com.domain.osgi.java.internal;
        
        import com.domain.osgi.java.HelloJavaService;
        
        public final class HelloJavaServiceImpl implements HelloJavaService {
            
            public void hello() {
                System.out.println("Hello, in Java");
            }
        }
    

Here we implement our HelloJavaService and say hello to System.out. Things are still very straight forward with no traces of OSGi code. Now let's create a BundleActivator named HelloJavaActivator.java so that we can actually register our service for consumption by other bundles.

        package com.domain.osgi.java.internal;
        
        import org.osgi.framework.BundleActivator;
        import org.osgi.framework.BundleContext;
        import org.osgi.framework.ServiceRegistration;
        
        import com.domain.osgi.java.HelloJavaService;
        
        public final class HelloJavaActivator implements BundleActivator {
            
            ServiceRegistration serviceRegistration;
            
            public void start(BundleContext bundleContext) throws Exception {
                System.out.println("STARTING com.domain.osgi.java");
                serviceRegistration = bundleContext.registerService(HelloJavaService.class.getName(), new HelloJavaServiceImpl(), null);
                System.out.println("REGISTERED com.domain.osgi.java.HelloJavaService");
            }
            
            public void stop(BundleContext bundleContext) throws Exception {
                System.out.println("STOPPING com.domain.osgi.java");
                serviceRegistration.unregister();
                System.out.println("UNREGISTERED com.domain.osgi.java.HelloJavaService");
            }
            
        }
    

Here things get a bit more interesting. org.osgi.framework.BundleActivator is an interface that may be implemented when a bundle is started or stopped. In the start method, we're registering our service with the framework via the BundleContext. Then we tie up all the loose ends in the stop method. Note that unregistering the service in the stop method isn't really required since the OSGi life cycle will manage that automatically.

Lastly, we'll need to update the osgi.bnd file in the root bundle directory; hello-java. The Bundle-Activator property was automatically generated by PAX and pointed to the Activator class that was automatically generated. Update it as follows so that our HelloJavaActivator implementation will be invoked when our bundle is started or stopped.

        #-----------------------------------------------------------------
        # Use this file to add customized Bnd instructions for the bundle 
        #-----------------------------------------------------------------
        
        Bundle-Activator: ${bundle.namespace}.internal.HelloJavaActivator
    

Now let's start things up with PAX Runner which will leverage Felix by default. This is accomplished with the pax:provision maven goal. In order for our bundle to be accessible at runtime, we need to package it up and install it in our local maven repository. Thanks to maven and the pax plugin, this is all accomplished with a simple one line command:

        mvn install pax:provision
        
        ...
        
        Welcome to Felix.
        =================
        
        -> STARTING com.domain.osgi.java
        REGISTERED com.domain.osgi.java.HelloJavaService
    

If you've done everything properly, and I haven't neglected any important details in this blog post, you should see the output from our start method in your BundleActivator. Now go ahead and execute the shutdown command to exit out of Felix and return to your shell:

        -> shutdown
        -> STOPPING com.domain.osgi.java
        UNREGISTERED com.domain.osgi.java.HelloJavaService
    

Good stuff so far. I won't dwell on it here, since folks like Craig Walls have covered it so well, but another huge advantage of using PAX is that it makes switching between the various OSGi frameworks incredibly simple. pax:provision is utilizing Pax Runner behind the scenes. Using the --platform switch (-Dframework=equinox in maven), you can easily fire up this example in Equinox or Knopflerfish or Concierge without /any/ changes to your code or configuration. I think that speaks volumes about the maturity of OSGi. Let's not forget that OSGi has been around for 10 years now! Don't be fooled by the nay-sayers who claim that OSGi isn't ready for prime time.

Groovy

So we've got a pretty cool little Java OSGi bundle working. But that's not why you're here. You're thinking beyond Java and the cool kids are talking your ears off about all these other languages that you can leverage on the JVM like Groovy, Scala, Fan, Clojure, JRuby, Jython, etc, etc, etc. And all these new toys are touting big productivity gains to boot. I'm not going to weigh in on which language you should use. I had to limit the scope of this article, so I selected the languages that I'm already using regularly.

So let's branch out and set up a bundle using Groovy. First, be sure to cd back to the helloworld project root directory and then execute:

        mvn org.ops4j:maven-pax-plugin:create-bundle -Dpackage=com.domain.osgi.groovy -DbundleName=hello-groovy
    

As before, PAX will happily go ahead and create a new bundle for us. However, the out of the box PAX stuff is very Java-centric. As such, there are several modifications that must be made to our maven configuration to get Groovy working. First, in order to get compilation working for Groovy scripts, add the following gmaven-plugin definition into the <plugins> section of the helloworld/poms/compiled/pom.xml file.

        <plugin>
          <groupId>org.codehaus.groovy.maven</groupId>
          <artifactId>gmaven-plugin</artifactId>
          <version>1.0-rc-5</version>
          <executions>
            <execution>
              <goals>
                <goal>generateStubs</goal>
                <goal>compile</goal>
                <goal>generateTestStubs</goal>
                <goal>testCompile</goal>
              </goals>
            </execution>
          </executions>
        </plugin>
    

Edit the parent pom.xml file located at helloworld/pom.xml and add a <dependency> for gmaven-runtime-1.6 like so:

        <dependency>
          <groupId>org.codehaus.groovy.maven.runtime</groupId>
          <artifactId>gmaven-runtime-1.6</artifactId>
          <version>1.0-rc-5</version>
        </dependency>
    

Lastly, we need to add a bundle dependency in order to have a proper Groovy bundle available to the OSGi framework at runtime.

        mvn pax:import-bundle -DgroupId=org.codehaus.groovy -DartifactId=groovy-all -Dversion=1.6.2
    

That will add the following to helloworld/provision/pom.xml:

        <dependencies>
          <dependency>
            <groupId>org.codehaus.groovy</groupId>
            <artifactId>groovy-all</artifactId>
            <version>1.6.2</version>
          </dependency>
        </dependencies>
    

With the maven configuration out of the way, cd into your hello-groovy bundle remove the generated Java sources and then we'll add 3 groovy scripts that are analogous to the Java classes we added earlier. Note that there are quite a number of ways you could package all of this stuff up to reduce the number of lines of code. I'm showing complete examples, each broken into their own bundle, so that you could take any one of these bundle examples independently and be off and running.

        rm -rf src/main/java;
        
        mkdir -p src/main/groovy/com/domain/osgi/groovy/internal;
        
        cd src/main/groovy/com/domain/osgi/groovy;
    

Here's our HelloGroovyService.groovy interface:

        package com.domain.osgi.groovy
        
        interface HelloGroovyService {
            void hello()
        }
    

And the corresponding HelloGroovyServiceImpl.groovy implementation:

        package com.domain.osgi.groovy.internal
        
        import com.domain.osgi.groovy.HelloGroovyService
        
        class HelloGroovyServiceImpl implements HelloGroovyService {
            void hello() {
                println "Hello, from Groovy"
            }
        }
    

Then we'll create our HelloGroovyActivator.groovy script. There's another small catch related to Groovy and OSGi. As noted in the Groovy documentation, publishing an OSGi service written in Groovy requires one extra step that a Java service does not. This is because of the way Groovy makes extensive use of ClassLoaders and reflection. When registering a service with the BundleContext, you must be sure to temporarily set the current thread's ContextClassLoader to the target object's ClassLoader, and then set it back when you're done.

        package com.domain.osgi.groovy.internal
        
        import org.osgi.framework.BundleActivator
        import org.osgi.framework.BundleContext
        import org.osgi.framework.ServiceRegistration
        
        import com.domain.osgi.groovy.HelloGroovyService
        
        class HelloGroovyActivator implements BundleActivator {
            
            ServiceRegistration serviceRegistration
            
            void start(BundleContext bundleContext) {
                ClassLoader originalClassLoader = Thread.currentThread().contextClassLoader
                try {
                    println "STARTING com.domain.osgi.groovy"
                    
                    Thread.currentThread().contextClassLoader = getClass().classLoader
                    HelloGroovyService helloGroovyService = new HelloGroovyServiceImpl()
                    serviceRegistration = bundleContext.registerService(HelloGroovyService.class.getName(), helloGroovyService, null)
                    
                    println "REGISTERED com.domain.osgi.groovy.HelloGroovyService"
                } finally {
                    Thread.currentThread().contextClassLoader = originalClassLoader
                }
            }
            
            void stop(BundleContext bc) {
                println "STOPPING com.domain.osgi.groovy"
                serviceRegistration.unregister()
                println "UNREGISTERED com.domain.osgi.groovy.HelloGroovyService"
            }
            
        }
    

Aside from the Classloader wizardry, this BundleActivator code should feel very familiar given the Java example above. Our final step is to update the helloworld/hello-groovy/osgi.bnd configuration so that everything gets packaged up properly:

        #-----------------------------------------------------------------
        # Use this file to add customized Bnd instructions for the bundle 
        #-----------------------------------------------------------------
        
        Bundle-Activator: ${bundle.namespace}.internal.HelloGroovyActivator
    

Then go ahead and fire everything up once more.

        mvn install pax:provision
        
        ...
        
        Welcome to Felix.
        =================
        
        -> STARTING com.domain.osgi.java
        REGISTERED com.domain.osgi.java.HelloJavaService
        STARTING com.domain.osgi.groovy
        REGISTERED com.domain.osgi.groovy.HelloGroovyService
    

And then shut everything down once more.

        -> shutdown
        -> STOPPING com.domain.osgi.groovy
        UNREGISTERED com.domain.osgi.groovy.HelloGroovyService
        STOPPING com.domain.osgi.java
        UNREGISTERED com.domain.osgi.java.HelloJavaService
    
Scala

I'm not going to get into any detail on the language specifics for Scala. If you're coming from a Java background and you're interested in a primer I would highly recommend Daniel Spiewak's series on Scala for Java Refugees.

We've been through the creation of two bundles so far so I'll pick up the pace a bit. The mechanics to generate the bundle are the same:

       mvn org.ops4j:maven-pax-plugin:create-bundle -Dpackage=com.domain.osgi.scala -DbundleName=hello-scala
    

We'll need to remove the Java sources and set up our package structure for Scala:

        rm -rf src/main/java;
        
        mkdir -p src/main/scala/com/domain/osgi/scala/internal;
        
        cd src/main/scala/com/domain/osgi/scala;
    

Then, to get compilation working for Scala, re-edit the helloworld/poms/compiled/pom.xml file and add the maven-scala-plugin.

        <plugin>
            <groupId>org.scala-tools</groupId>
            <artifactId>maven-scala-plugin</artifactId>
            <version>2.9.1</version>
            <executions>
                <execution>
                    <goals>
                        <goal>compile</goal>
                        <goal>testCompile</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    

The parent pom.xml file in the helloworld project directory gets tweaked to add a <dependency> for scala-library like so:

        <dependency>
            <groupId>org.scala-lang-osgi</groupId>
            <artifactId>scala-library</artifactId>
            <version>2.7.4</version>
        </dependency>
    

As with our Groovy configuration, we need to add a bundle dependency in order to have a proper Scala bundle available to the OSGi framework at runtime.

        mvn pax:import-bundle -DgroupId=org.scala-lang-osgi -DartifactId=scala-library -Dversion=2.7.4
    

That will add the following to helloworld/provision/pom.xml:

        <dependency>
            <groupId>org.scala-lang-osgi</groupId>
            <artifactId>scala-library</artifactId>
            <version>2.7.4</version>
        </dependency>
    

And then we'll stamp out some code:

HelloScalaService.scala:

        package com.domain.osgi.scala
        
        trait HelloScalaService {
          def hello()
        }
    

HelloScalaServiceImpl.scala:

        package com.domain.osgi.scala.internal
        
        import com.domain.osgi.scala._
        
        class HelloScalaServiceImpl extends HelloScalaService {
            def hello() = {
                Console.println("Hello, from Scala")
            }
        }
    

HelloScalaActivator.scala:

        package com.domain.osgi.scala.internal
        
        import org.osgi.framework._
        import com.domain.osgi.scala.HelloScalaService
        
        class HelloScalaActivator extends BundleActivator {
          
          var serviceRegistration:ServiceRegistration = _
          
          def start(bundleContext: BundleContext) {
            Console.println("STARTING com.domain.osgi.scala")
            serviceRegistration = bundleContext.registerService("com.domain.osgi.scala.HelloScalaService", new HelloScalaServiceImpl(), null)
            Console.println("REGISTERED com.domain.osgi.scala.internal.HelloScalaService")
          }
          
          def stop(context: BundleContext) {
            Console.println("STOPPED com.domain.osgi.scala")
            if (serviceRegistration != null) serviceRegistration.unregister
            Console.println("UNREGISTERED com.domain.osgi.scala.internal.HelloScalaService")
          }
          
        }
    

As with the previous bundles, our final step is to update the helloworld/hello-scala/osgi.bnd configuration so that it reads:

        #-----------------------------------------------------------------
        # Use this file to add customized Bnd instructions for the bundle 
        #-----------------------------------------------------------------
        
        Bundle-Activator: ${bundle.namespace}.internal.HelloScalaActivator
    

While we didn't need to jump through any coding hoops for Scala in the way we did for Groovy, there's /is/ one final maven configuration detail that we need to wrangle before this Scala example will work. If you were paying really close attention, you may have noticed that the Scala OSGi bundle that we imported had a groupId of org.scala-lang-osgi. At the time of this writing, the scala jars aren't packaged with OSGi support. Proper support is forthcoming, but in the mean time, the good folks behind Scala are releasing jars in parallel that /do/ work with OSGi. In order to pick those up, we need to add the scala-tools.org repository to our project. A quick call to pax:add-repository from the parent helloworld project directory will save some typing.

        mvn pax:add-repository -DrepositoryId=scala-tools.org -DrepositoryURL=http://scala-tools.org/repo-releases
    

This repository definition will be appended to your pom.xml.

        <repositories>
          <repository>
            <id>scala-tools.org</id>
            <url>http://scala-tools.org/repo-releases</url>
            <snapshots>
              <enabled>false</enabled>
            </snapshots>
          </repository>
        </repositories>
    

Now, we can install and provision one last time to see all three bundles activated.

        mvn install pax:provision
        
        ...
        
        Welcome to Felix.
        =================
        
        -> STARTING com.domain.osgi.java
        REGISTERED com.domain.osgi.java.HelloJavaService
        STARTING com.domain.osgi.groovy
        REGISTERED com.domain.osgi.groovy.HelloGroovyService
        STARTING com.domain.osgi.scala
        REGISTERED com.domain.osgi.scala.internal.HelloScalaService
    

As expected, issuing the shutdown command will yield all the proper shutdown messaging.

        -> shutdown
        -> STOPPED com.domain.osgi.scala
        UNREGISTERED com.domain.osgi.scala.internal.HelloScalaService
        STOPPING com.domain.osgi.groovy
        UNREGISTERED com.domain.osgi.groovy.HelloGroovyService
        STOPPING com.domain.osgi.java
        UNREGISTERED com.domain.osgi.java.HelloJavaService
    

Conclusion

This ended up being a much longer article than I anticipated but we've covered a lot of ground. Maven has worked it's dependency voodoo which saved an enormous amount of time downloading jars and messing with classpaths. We've seen how the PAX toolkit from OPS4J makes creating, modifying and provisioning OSGi bundles a breeze. While the actual code examples were pretty trivial, we successfully managed to code up bundles in Java, Scala and Groovy. I think this displays a lot of the power that is offered by OSGi and points to a bright future for enterprise development on the JVM.

Check out Part 2 here