BT

Modular Java: Dynamic Modularity

Posted by Alex Blewitt on Nov 12, 2009 |

In the third of the Modular Java series, we'll cover dynamic modularity. We'll describe how a bundle's classes are resolved, how they can come and go, and how they can communicate with each other.

The previous instalment, Modular Java: Static Modularity, described how Java modules can be built and deployed as separate JARs. The example gave a client and server bundle (both in the same VM), and the client found the server via a factory method. In that example, the factory was instantiating a known class, but equally could have used reflection to obtain a service implementation; Spring uses this technique heavily to bind spring objects together.

Before we cover dynamic services, it's worth taking a step back to consider class paths, since one of the differences between standard Java code and modular Java code is how dependencies are bound at runtime. Once we've covered that, we'll briefly touch on garbage collection of classes; so if are comfortable with that, you can skip ahead.

Bundle ClassPath

In a flat Java program, there is only one classpath — the one that the application was started with. This is usually specified on the command line, with -classpath, or via the CLASSPATH environment variable. The Java class loader then scans this path when trying to resolve classes at runtime, whether statically (compiled into the code) or dynamically (using reflection and class.forName()). However, it's possible to use multiple classloaders at runtime; web application engines like Jetty and Tomcat frequently use this in order to support hot (re)deployment of applications.

In OGSi, each bundle has its own class loader. Classes accessed from other bundles are delegated to the other bundle's class loader. So whilst in a traditional application, classes from a logging library, a client and server JAR may be loaded by the same class loader, in an OSGi module system, each would be loaded by their own.

One corollary for this is that it's possible to have multiple class loaders in a VM which have different Class objects with the same name. A class called com.infoq.example.App could be exported both by version 1 and version 2 of bundle com.infoq.example in the same VM. Client bundles bound to version 1 would get the version 1 class, and clients bound to version 2 would get the version 2 class. This is a fairly common occurrence for modular systems; some code might need to load an older version of a library whilst newer code (in another bundle) might need a newer version of a library, inside the same VM. Fortunately, OSGi manages such transitive dependencies for you, and ensures that you never have problems due to incompatible classes.

Garbage collection of classes

Each class has a reference to the class loader that defined it. So if you were to access classes from a different bundle, you not only have the explicit reference to (the instance of) the class, but also its class loader. Whilst one bundle holds onto another's classes, it pins that bundle into memory. In the previous example, the client is pinned to the server.

In a static world, it doesn't matter if you pin yourself to another class (or library); nothing comes and goes. However, in a dynamic world, it's possible for libraries or utilities to be replaced at runtime with newer versions. This might sound complex, but in fact it's been happening since the early days of web application engines like Tomcat (first released in 1999) with hot deployment of web applications. Each web application binds to the version of the Servlet API, and when it's stopped, the class loader that loaded the web application is dropped. When the web application is redeployed, a new class loader is created, and new versions of the classes are loaded in. As long as the servlet engine doesn't attempt to hold on to references from the old application, then the classes become garbage collected just like any other Java object.

Not all libraries are aware of the possibilities of class leaks, which like memory leaks, are possible to code in Java. An obvious example is Log4J's addAppender() call, which once executed, will bind your classes to the lifetime of Log4J's bundle. Even if your bundle is stopped, Log4J will maintain the references to the appender and continue to send logging events (unless the bundle calls the appropriate removeAppender() method when it stops).

Finding and binding

In order to be dynamic, we need to have a mechanism by which we can lookup services, but not permanently hold onto them (in case the bundle goes away). This is achieved through the use of simple Java interfaces and POJOs, known as services. (Note that they don't have any relation to WS-DeathStar or any other XML heavy infrastructure; they're just Plain Old Java Objects.)

Whereas typical factories will be implemented using some form of class name acquired via a properties file and a subsequent Class.forName(), OSGi maintains a 'service registry' – in essence, a map containing a list of class names and services. So, instead of using class.forName("com.example.JDBCDriver") to acquire a JDBC driver, an OSGi system could use context.getService(getServiceReference("java.sql.Driver")) instead. This frees up the client code from having to know about any specific client implementation; instead, it can bind to whatever driver(s) are available at runtime. Migrating to a different database server would be a case of stopping one module and starting a new module; the client wouldn't even need to be restarted, nor would any configuration need to be changed.

The reason this works is that the client only needs to know the API to the service it is requesting. This is almost always an interface, although the OSGi specification permits any class to be used. In the above case, the interface name is java.sql.Driver; the instance of the interface that's returned is the database implementation (whose class isn't known about, or indeed coded anywhere). Furthermore, if the service isn't available (there isn't a database, or the database has been temporarily stopped), then this method returns null to indicate that no such service is available.

In order to be fully dynamic, the return result shouldn't be cached. In other words, each time the service is required, the getService needs to be re-invoked. The framework performs caching under the covers, so this isn't too much of a performance concern. But importantly, it allows the database service to be replaced on the fly with a new service, without changing the code – at the next invocation, the client will transparently bind to the new service.

Putting it into action

In order to demonstrate this, we're going to create an OSGi service that's useful for shortening URLs. The idea of these (largely interchangable) services is to take a long URL, like http://www.infoq.com/articles/modular-java-what-is-it, and convert that into a shorter URL, like http://tr.im/EyH1. As well as being used extensively on sites like Twitter, it's also possible to use these to replicate otherwise long URLs into something that can be written down on the back of a post-it note. Even magazines like New Scientist and Macworld are using them for printed media links.

In order to implement the service, we'll need:

  • An interface to a shortening service
  • A bundle that, upon starting, will register a shortening implementation
  • A demonstration client

Although there's nothing preventing these all being in the same bundle, we'll put them separate bundles. (Even when they are in the same bundle, it's best practice to allow the bundles to communicate via services as if they were in separate bundles; this makes it easier for them to integrate with other providers.)

It's important that the interface to the shortening service is in a separate bundle from any implementations (or clients). The interface represents the 'shared code' between client and server, and as such, gets loaded by every bundle. Since this effectively pins each bundle to the (specific version of the) interface for the collective lifetime of all the services, by placing it in a separate bundle (which will remain running throughout the lifetime of the OSGi VM), we can allow our clients to come and go. If we were to put the interface in the same bundle as one of the service implementations, we wouldn't be able to reconnect clients if that service came and went.

The manifest and implementation for the shorten interface isn't that interesting:

Bundle-ManifestVersion: 2
Bundle-Name: Shorten
Bundle-SymbolicName: com.infoq.shorten
Bundle-Version: 1.0.0
Export-Package: com.infoq.shorten
--- 
package com.infoq.shorten;

public interface IShorten {
	public String shorten(String url) throws IOException;
}

All this does is set up a bundle (com.infoq.shorten) with a single interface (com.infoq.shorten.IShorten), which is then exported to clients. The argument will just take a URL and then return an abbreviated version of the same.

The implementation is a little more interesting. The grand-daddy of them all is TinyURL.com, although more recently, shorter names have started to crop up. (It's somewhat ironic that http://tinyurl.com can actually be abbreviated to a smaller URL like http://ow.ly/AvnC). Various popular ones exist; ow.ly, bit.ly, tr.im etc. This isn't meant to be a comprehensive guide to (or endorsement of) any of them; the implementation could apply equally to other services as well. This article will use TinyURL and Tr.im, for no other reason than they have anonymous GET-based submissions which makes them easy to implement.

The implementation of both clients are in fact remarkably similar; they both take a URL with a parameter (the thing that is being shortened) and then return the newly shortened text:

package com.infoq.shorten.tinyurl;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.URL;
import com.infoq.shorten.IShorten;

public class TinyURL implements IShorten {
	private static final String lookup = 
		"http://tinyurl.com/api-create.php?url=";
	public String shorten(String url) throws IOException {
		String line = new BufferedReader(
			new InputStreamReader(
				new URL(lookup + url).openStream())).readLine();
		if(line == null)
			throw new IllegalArgumentException( 
				"Could not shorten " + url);
		return line;
	}
}

The implementation for Tr.im is similar, except for using the URL http://api.tr.im/v1/trim_simple?url= as the lookup instead. The source for both are in the com.infoq.shorten.tinyurl and com.infoq.shorten.trim bundles.

So, armed with an implementation of a shortening service, how do we enable others to access it? Well, we need to register this as a service to the OSGi framework. The method registerService(class,instance,properties) on the BundleContext class allows us to define a service for later use, and is typically called during the bundle's start() call. As covered last time, this means we have to have a BundleActivator defined. As well as the implementation of the class, we have to remember to put the Bundle-Activator into the MANIFEST.MF in order to find the implementation. Here's what it will look like:

Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: TinyURL
Bundle-SymbolicName: com.infoq.shorten.tinyurl
Bundle-Version: 1.0.0
Import-Package: com.infoq.shorten,org.osgi.framework
Bundle-Activator: com.infoq.shorten.tinyurl.Activator
---
package com.infoq.shorten.tinyurl;
import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;
import com.infoq.shorten.IShorten;

public class Activator implements BundleActivator {
	public void start(BundleContext context) {
		context.registerService(IShorten.class.getName(),
			new TinyURL(),null);
	}
	public void stop(BundleContext context) {
	}
}

Although the registerService() method takes a string as its first argument, and it would be equally valid to say "com.infoq.shorten.IShorten", it's best practice to use class.class.getName(), as if you refactor the package or change the class name, it will be caught by the compiler. If you just use a string, and perform a bad refactoring, then you won't know about the problem until runtime.

The second argument for registerService() is the instance itself. The reason this is distinct from the first argument is so that you can have the same service instance export multiple service interfaces (useful if you have a versioned API in your requirements, as you can evolve your interfaces). In addition, it's quite possible that a single bundle will export multiple services of the same type.

The last argument is for the service properties. These allow you to annotate the service with extra metadata, such as indicating a preference for how important this service is with respect to others, or additional information which may be of interest to the caller (such as a description and a vendor).

As soon as this bundle starts, the shortening service will be made available to clients. When the bundle stops, the framework will automatically un-register any services. We could un-register it earlier (using context.unregisterService()) if we wanted to (say, in response to an error code or a network interface not being available).

Using the service

Once the service is up and running, we can then use a client to access it. If you're running in Equinox, you can use the services command to list the installed services, and who they are registered by:

{com.infoq.shorten.IShorten}={service.id=27}
  Registered by bundle: com.infoq.shorten.trim-1.0.0 [1]
  No bundles using service.
{com.infoq.shorten.IShorten}={service.id=28}
  Registered by bundle: com.infoq.shorten.tinyurl-1.0.0 [2]
  No bundles using service.

The client will need to resolve the service, before invoking it with a URL. We need to get a service reference, which allows us to introspect properties of the service itself, and then use that to get the service that we're interested in. However, we're going to need to be able to do this repeatedly (and with different URLs) so we can integrate it into the Equinox or Felix shells. Here's what the implementation will do:

package com.infoq.shorten.command;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceReference;
import com.infoq.shorten.IShorten;

public class ShortenCommand {
	protected BundleContext context;
	public ShortenCommand(BundleContext context) {
		this.context = context;
	}
	protected String shorten(String url) throws IllegalArgumentException, IOException {
		ServiceReference ref =
			context.getServiceReference(IShorten.class.getName());
		if(ref == null)
			return null;
		IShorten shorten = (IShorten) context.getService(ref);
		if(shorten == null)
			return null;
		return shorten.shorten(url);
	}
}

When the shorten method is invoked, this piece of code will look up the service reference, and from that, get the service object. We can then cast it to an IShorten object and use it to interact with the service registered previously. Note that all of this is in the same VM; there's no remote calls, no mandatory exceptions, no arguments are being serialised; it's just one POJO talking to another POJO. In fact, the only difference between this and the initial class.forName() example is how we're obtaining the shorten POJO.

In order to use this inside Equinox and Felix, we need to put in some boilerplate code. Suffice to say, that when we define our manifest, we can declare optional dependencies on both the Felix and Equinox command line interfaces, so that when we install into either, we'll be able to run. (A better solution might be to deploy these as separate bundles so that we can lose the optionality; but the activator will fail if the bundles aren't present, and so won't be startable.) The source for the Equinox and Felix specific command hookups are in the com.infoq.shorten.command bundle for the curious.

The result is that if we install the command client bundle, we'll get a new command, shorten, which we can invoke from an OSGi shell. This is run if you run java -jar equinox.jar -console -noExit or java -jar bin/felix.jar. You'll need to perform an install of the bundles in order to work; thereafter, you'll be able to use the commands:

java -jar org.eclipse.osgi_* -console -noExit
osgi> install file:///tmp/com.infoq.shorten-1.0.0.jar
Bundle id is 1
osgi> install file:///tmp/com.infoq.shorten.command-1.0.0.jar
Bundle id is 2
osgi> install file:///tmp/com.infoq.shorten.tinyurl-1.0.0.jar
Bundle id is 3
osgi> install file:///tmp/com.infoq.shorten.trim-1.0.0.jar
Bundle id is 4
osgi> start 1 2 3 4
osgi> shorten http://www.infoq.com
http://tinyurl.com/yr2jrn
osgi> stop 3
osgi> shorten http://www.infoq.com
http://tr.im/Eza8

Note that both the TinyURL and Tr.im services are available at run-time, but clearly only one service can be used at a time. It's possible to set up a service ranking, which is an integer between Integer.MIN_VALUE and Integer.MAX_VALUE, by putting a corresponding value in with the Constants.SERVICE_RANKING key when the service is first registered. Higher values indicate a higher ranking, and when services are queried, the highest-ranking service will be returned. In the absence of a service ranking (which defaults to zero), or in a tie-break with multiple services, the automatically assigned Constants.SERVICE_PID is used to arbitrarily order the services.

The other thing to note is that when we'd stopped the other service, the client automatically failed over to the next service in the list. Each time the command is run, it obtains the (current) service to use for its shortening needs. If the service providers change between runs, then the command need not be concerned, only that there is one when it needs it. (If you stop all providers, the service lookup will return null which will result in an error being printed – good code should ensure that it defensively programs against the possibility of a null being returned for the service reference.

Service Tracker

Instead of having to look up services each time, it's possible to use a ServiceTracker to do the work instead. This skips out the intermediary step of obtaining a ServiceReference, but does require that you call open after construction, in order to begin tracking services.

As with the ServiceReference, it's possible to call getService() to obtain the service instance. There's also waitForService(), which blocks if a service is unavailable for the specified timeout (or for ever, if the timeout is zero). We could re-implement the shortening command as follows:

package com.infoq.shorten.command;

import java.io.IOException;
import org.osgi.framework.BundleContext;
import org.osgi.util.tracker.ServiceTracker;
import com.infoq.shorten.IShorten;

public class ShortenCommand {
	protected ServiceTracker tracker;
	public ShortenCommand(BundleContext context) {
		this.tracker = new ServiceTracker(context,
			IShorten.class.getName(),null);
		this.tracker.open();
	}
	protected String shorten(String url) throws IllegalArgumentException,
			IOException {
		try {
			IShorten shorten = (IShorten)
				tracker.waitForService(1000);
			if (shorten == null)
				return null;
			return shorten.shorten(url);
		} catch (InterruptedException e) {
			return null;
		}
	}
}

A common problem with Service Tracker is forgetting to invoke open() once it's constructed. It will also be necessary to import org.osgi.util.tracker as a package inside the MANIFEST.MF in order to run.

Using ServiceTracker in order to manage dependencies on services is generally seen as a good way to manage relationships. There are some subtle complexities in looking up a service which are exposed when not using a service, such as when a ServiceReference becomes unavailable but before it is resolved into a service. The rationale for having a ServiceReference are that it's possible for the same instance to be shared across bundles, and it can be used to (manually) filter out services based on some criteria. However, it's also possible to use filters in order to restrict the set of available services.

Service properties and filters

When a service is registered, it's possible to register service properties with it. Most of the time this can be null, but it's possible to supply both OSGi-specific and general properties about the URLs. For example, let's say that we want to rank the services in order of preference. We can register the Constants.SERVICE_RANKING as part of the initial registration process with some form of numeric preference value. We might also want to put in some metadata that the client may wish to know, such as where the home page of the service is, and a link to the terms and conditions for the site. To do this, we'll need to modify our activator:

public class Activator implements BundleActivator {
	public void start(BundleContext context) {
		Hashtable properties = new Hashtable();
		properties.put(Constants.SERVICE_RANKING, 10);
		properties.put(Constants.SERVICE_VENDOR, "http://tr.im");
		properties.put("home.page", "http://tr.im");
		properties.put("FAQ", "http://tr.im/website/faqs");
		context.registerService(IShorten.class.getName(),
			new Trim(), properties);
	}
...
}

The service ranking is automatically managed by the ServiceTracker and others, but it's possible to filter out those with certain attributes as well. The Filter is compiled from an LDAP style filter, which uses a prefix notation to perform multiple filters. Although it's most common that you want to supply the name of the class (Constants.OBJECTCLASS), you can also perform tests on values (and even constrain ranges of continuous variables). Filters are created via the BundleContext; if we wanted to track services which implemented the IShorten interface as well as having a FAQ defined, we could do:

...
public class ShortenCommand
	public ShortenCommand(BundleContext context) {
		Filter filter = context.createFilter("(&" +
			"(objectClass=com.infoq.shorten.IShorten)" +
			"(FAQ=*))");
		this.tracker = new ServiceTracker(context,filter,null);
		this.tracker.open();
	}
	...
}

Standard properties, which can be filtered on or set when defining a service, include:

  • service.ranking (Constants.SERVICE_RANKING) - an integer that can be used to prioritise services over others
  • service.id (Constants.SERVICE_ID) - an integer, automatically set by the framework when the service is registered
  • service.vendor (Constants.SERVICE_VENDOR) - a string which can be set to indicate who the service comes from
  • service.pid (Constants.SERVICE_PID) - a string, or array of strings, representing the service's persistent identifier
  • service.description (Constants.SERVICE_DESCRIPTION) - the description of the service
  • objectClass (Constants.OBJECTCLASS) - the list of interfaces that this service is registered under

The filter syntax is defined in Section 3.2.7 "Filter syntax" of the OSGi core specification. Essentially, it allows operations like equality (=), approximation (~=), greater-or-equal, lesser-or-equal as well as substring comparisons. Brackets group the filters, and can be combined with &, | or ! modifiers for and, or and not, respectively. Whilst the attribute names are not case sensitive, the values may be (unless compared with ~=). The * character is used to indicate wildcards, and can be used to support substring matching as in com.infoq.*.*.

Summary

In this article, we've explored how we can use service as a way of communicating between bundles instead of direct class references. Services allow the module system to be dynamic, such that it reacts to services which come and go at runtime. We've also touched upon service ranking, properties and filters, and used the standard service tracker to make it easy to access and track services that come and go. We'll be looking at how to make wiring of services easier with declarative services in the next installment.

Installable bundles (also containing source):

Hello stranger!

You need to Register an InfoQ account or or login to post comments. But there's so much more behind being registered.

Get the most out of the InfoQ experience.

Tell us what you think

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

Email me replies to any of my messages in this thread

Thanks by Lars Vogel

Thanks for this example, I usually struggle to find a small but still useful example for a tiny OSGi service but this one is really good.

on the fly update by Giuseppe Sarno

Hello, excellent article,
I have playied with the example I was wondering what do I have to do in order to handle the situation where the service is "updated" (bundle update from the console). Would the Service be replaced while the client is invoking requests ?
Regards.

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

Email me replies to any of my messages in this thread

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

Email me replies to any of my messages in this thread

2 Discuss

Educational Content

General Feedback
Bugs
Advertising
Editorial
InfoQ.com and all content copyright © 2006-2014 C4Media Inc. InfoQ.com hosted at Contegix, the best ISP we've ever worked with.
Privacy policy
BT