Monday, September 8, 2008

SPI in OSGi

If you search for OSGi on the net you will find lots of references to class loading problems. Many, if not most, of those problems are related to SPIs, or Service Provider Interfaces, a.k.a. plugins, a.k.a. extensions. The developer provides a collaborating mechanism for other developers that he has no direct connection with. An SPI usually consists of some interface that needs to be implemented by a service provider. The hard problem is linking that service provider's code into the main own code. Because the developers have no clue who will provide extensions, they can not just new an implemenation class. They need some mechanism that provides them the name of the implementation class so they can create an instance with Class.forName and newInstance.

One popular mechanism in the Java VM is the service model. In this model, a JAR can contain a file in META-INF/services that has the fully qualified name of the SPI class, usually a factory. When you the getDefault, or getInstance, method on the SPI class, it reads that file and gets the name of the implementation class. It then tries to load that class. However, this process is not well supported with an API and it is therefore sometimes hard to find that class, causing the developers to do class loader hacks. Hacks that tend to work in the anarchistic class path of the VM but that do not work in OSGi because of the modularity restrictions. (How can you load the proper class if all you have is a class name and no version information?) In several blogs I complained about doing these hacks to get extensibility, but I never showed how you could do an SPI model in OSGi.

A very popular way in OSGi to do SPIs is the white board pattern. The white board pattern means that providers just register a service and the SPI code just enumerates all these services. As an example, we create a little portal. This is a servlet that lists a number of HtmlFragment services on the left side, and when you click on their link, it will list the an HTML fragment on the right side of the page.

Disclaimer: the following code ignores error checking to focus on the extension mechanism. Please do not code like this in production systems ...

We start with the service definition. In OSGi, a service is usually defined in an interface. You can use a class but it is more flexible to use an interface. In this case we define the HtmlFragment interface.

package my.service;
import java.io.*;
import javax.servlet.http.*;
public interface HtmlFragment {
String NAME = "fragment.name";
String TITLE = "fragment.title";

void write(
HttpServletRequest request,
PrintWriter pw) throws Exception;
}
This interface defines properties for the name of the Html Fragment and the title of the Html Fragment. The name is the last part of the URL, the title is human readable. The write method is called when the fragment should write something to the screen. The parameters are passsed in an HttpServletRequest. With this interface we can write an example fragment. In good style, we create an Hello World fragment.

package my.fragment.hello;
import java.io.*;
import javax.servlet.http.*;
import aQute.tutorial.service.portal.*;

public class Component implements
HtmlFragment {
public void write(
HttpServletRequest request,
PrintWriter pw) throws Exception {
pw.println("Hello World");
}
}
The bnd code to run this (assuming you use declarative services) is:
Private-Package: my.fragment.hello
Service-Component: my.fragment.hello.Component;\
provide:=my.service.HtmlFragment; \
properties:="fragment.name=agent,fragment.title=Agent"

It should be obvious, that you could creat many different Html Fragment services, each providing their own content.

The next code we need is the portal code. This can be a servlet. You register a servlet with an Http Service. Declarative services makes this quite trivial. So this is all there is to the portal code:
package my.portal;

import org.osgi.framework.*;
import org.osgi.service.component.*;
import org.osgi.service.http.*;

public class Portal {
BundleContext context;

protected void activate(
ComponentContext context) {
this.context = context
.getBundleContext();
}

public void setHttp(HttpService http)
throws Exception {
http
.registerServlet("/portal",
new PortalServlet(context), null,
null);
}

public void unsetHttp(HttpService http) {
http.unregister("/portal");
}
}

Due to the needed HTML, the Portal Servlet is a bit bigger:

package my.portal;
import java.io.*;
import javax.servlet.http.*;
import org.osgi.framework.*;
import aQute.tutorial.service.portal.*;

class PortalServlet extends HttpServlet {
final BundleContext context;

PortalServlet(BundleContext context) {
this.context = context;
}
public void doGet(
HttpServletRequest request,
HttpServletResponse rsp)
throws IOException {
PrintWriter pw = rsp.getWriter();
try {
String path = request
.getPathInfo();

if (path != null
&& path.startsWith("/"))
path = path.substring(1);

pw
.println("Portal");
pw
.println("
Portal
\n");

ServiceReference found = null;
ServiceReference all[] = context.getServiceReferences(HtmlFragment.class.getName(), "(&(fragment.name=*)(fragment.title=*))");
for (ServiceReference ref : all ) {
String name = (String) ref
.getProperty(HtmlFragment.NAME);
String title = (String) ref
.getProperty(HtmlFragment.TITLE);

pw.printf(
"%s",
name, title);
if (name.equals(path))
found = ref;
}

pw.println("
");

if (found != null) {
HtmlFragment fragment = (HtmlFragment) context.getService(found);
fragment.write(request, pw);
}
pw
.println("
");
} catch (Exception e) {
e.printStackTrace();
}
}
}
That is all! Again, obviously no error checking but it demonstrates rather well how you can use the OSGi Service Registry to implement SPIs.

And did you realize this code was 100% dynamic?

Peter Kriens

1 comment:

  1. Hi Peter,

    Pax Web supports the Whiteboard extender to register servlets and Filters. I think it would be great if this could get into the standard.
    The whole lifecycle is much easier when using the whiteboard pattern.

    See: http://wiki.ops4j.org/display/paxweb/Whiteboard+Extender

    What do you think?

    Christian

    ReplyDelete