Monday, February 1, 2010

Step 3 – Service Consumption

OSGi has often been referred to as SOA inside a JVM and I think that’s a fair analogy. The beauty of OSGi is that you do not have to deal with much of the <ceremony /> involved in traditional web services approaches. Comparing OSGi to traditional SOA implementations shows that the complexity is lower, performance is better, and all of the power is still there with OSGi. The one exception to this rule is when your application needs to connect to non-JVM based dependency, you’ll still need to use web services.

In the last few posts, we’ve managed to create a domain bundle and create a rigged raffle bundle that makes use of it. Let’s change this implementation to have a domain agnostic service layer that can leverage the domain bundle under the hood (or a database, web service provider, or something else in the future). Attentive readers will know the drill by now. Start out by creating a service bundle using pax-create-bundle from the project directory.

Screen shot 2010-02-01 at 2.15.40 PM

Creating the service is pretty straightforward. We’ll provide a publicly accessible interface in the com.pillartech.raffle.service package.

package com.pillartech.raffle.service;
import java.util.Set;
public interface RaffleService {
  public void addEntry(String name, String email);
  public Set<String> pickWinners(int numOfWinners);
}


This service allows a consumer to add an entry, and pick  a certain number of winners which is all our rigged consumer really needs at this point. A common practice in OSGi is to create an internal package underneath this main package to store all of non-public classes. Only packages explicitly exposed in the manifest will be made public for other bundles, not directories underneath, so we’ll store our service implementation and activators in this internal directory. Create a com.pillartech.raffle.service.internal package and define a class within this package that implements the service interface. My implementation is fairly straightforward:



package com.pillartech.raffle.service.internal;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import com.pillartech.raffle.domain.Entry;
import com.pillartech.raffle.domain.Raffle;
import com.pillartech.raffle.service.RaffleService;
public class RaffleServiceImpl implements RaffleService {
  private Raffle raffle = null;
  public RaffleServiceImpl() {
    raffle = new Raffle();
  }
  public void addEntry(String name, String email) {
    Entry e = new Entry();
    e.setName(name);
    e.setEmail(email);
    e.setCreated(new Date());
    raffle.addEntry(e);
  }
  public Set<String> pickWinners(int numOfWinners) {
    raffle.setNumberOfWinners(numOfWinners);
    Set<Entry> winners = raffle.pickWinners();
    
    Set<String> winnerNames = new HashSet<String>();
    for (Entry entry : winners) {
      winnerNames.add(entry.getName());
    }
    return winnerNames;
  }
}


As you can see, this trivial example just stores the domain objects internally and translates the simplistic arguments passed in to domain objects within the raffle. This may seem like a zero sum gain compared to our last implementation and in all honesty it is. It does pave the way for our service bundle to do things like leveraging a database, and managing transactions across separate DAOs which we’ll accomplish in due time.  Now, we are one activator away from having a viable service published in our container. In the internal directory create an activator class and plumb in some code like this:



package com.pillartech.raffle.service.internal;
import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;
import com.pillartech.raffle.service.RaffleService;
public class RaffleServiceActivator implements BundleActivator {
  public void start(BundleContext bc) throws Exception {
    bc.registerService(RaffleService.class.getName(),
        new RaffleServiceImpl(), null);
  }
  public void stop(BundleContext bc) throws Exception {
  }
}

This activator leverages the BundleContext passed into the start method to register the service. We’re using the common practice of publishing the service using the fully qualified class name of the interface. At this time we could provide a file location, database connection, or other such dependency into the constructor of the implementation, but we’ll get to that soon enough. If you do provide such dependencies, make sure that they are cleaned up and destroyed in the stop method of the activator as well. All that is left is to update the BND file to the name of your activator and you should have a deployable service bundle. Feel free to run mvn install and pax-provision to test the waters.

Publishing the service is the easy part, we still need to consume this service from the rigged raffle bundle. From the rigged bundle directory, run a pax-import-bundle, providing the information used when creating the service.



Screen shot 2010-02-01 at 2.40.00 PM 



Now, we need to re-implement the activator in the rigged bundle to take advantage of the beautiful service waiting out there for us. My updated copy is this:



ackage com.pillartech.raffle.rigged.internal;
import java.util.Set;
import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;
import org.osgi.util.tracker.ServiceTracker;
import com.pillartech.raffle.service.RaffleService;
public final class ServiceBasedRiggedRaffleActivator implements BundleActivator {
  private ServiceTracker tracker;
  private RaffleService service;
  
  public void start(BundleContext bc) throws Exception {
    System.out.println("Grabbing a handle on the raffle service");
    tracker = new ServiceTracker(bc, RaffleService.class.getName(), null);
    tracker.open();
    
    service = (RaffleService) tracker.getService();
    if (service != null) {
      addEntrants();
    }
    else {
      System.out.println("Unable to rig the raffle, cannot get a handle on the service");
    }
  }
  private void addEntrants() {
    System.out.println("Adding entrants");
    final int COUNT = 10;
    for (int i = 0; i < COUNT; i++) {
      service.addEntry("Todd("+i+")", "toddkaufman@gmail.com");
    }
    System.out.println(COUNT + " entries added to the raffle.");
  }
  public void stop(BundleContext bc) throws Exception {
    System.out.println("And the winner of the raffle is ...");
    Set<String> winners = service.pickWinners(1);
    for (String winner : winners) {
      System.out.println(winner + " won!");
    }
    
    System.out.println("Releasing handle on the raffle service.");
    tracker.close();
  }
}


You’ll notice in this example we now have to make use of a ServiceTracker to access the published service. In OSGi land, services can come and go at any time. ServiceTracker’s can be used to grab a handle to a service and optionally wait if one is not available.  Leveraging the service tracker we then access the published service via the interface and interact with it as any normal java class would. Also, take note that we need to close the service tracker in the stop method to ensure that the integration of the two bundles is gracefully decoupled should the rigged raffle go away. If we would prefer to wait for the service should it not be available, we would call tracker.waitForService instead of getService and provide a time in ms to wait should the service not be available immediately. Craig Walls warns against waiting for services in the activator in his Modular Java book though as it may cause a traffic jam of bundles waiting to activate on startup. You should be able to execute a mvn install on the entire project and a pax-provision from the main directory to see everything up and running.



Screen shot 2010-02-01 at 2.55.18 PM



Beauty! We have a working consumer interacting with a service all through the OSGi container.  We’re starting to see how modular bundles can interact within an OSGi container, but if you are like me you are probably feeling pretty dirty about the amount of OSGi APIs that are leaking into your code. Testing these bundles in isolation would require a significant amount of mocking now and we had to manually construct a service implementation using the new keyword. Fear not, there are better, less invasive ways of accomplishing this and we’ll go through them in the next installment.



Examples as always available on github.

No comments: