Realizing A Service Provider Framework with Java EE / CDI

Consider the following scenario: you are writing some code that requires a service. There may be multiple implementations of the service, and possibly not all the implementations are known a priori. You’d like the client code not to depend on any particular implementation, but would like the implementation to be able to select a particular implementation at runtime and use it.

This is what is called service provider framework: this is a system where multiple service providers implement a service and the system makes the implementations available to it clients, decoupling them from the implementations. Of course, since the client code uses the implementations at some point of time, there is still a runtime dependency between the client code and the individual implementations of the service, but the important point is that the client code does not have to change when new implementations are introduced.

Let’s consider a very simple scenario: there is an interface, called ProviderInterface, and there can be multiple implementations unknown at design time (say, ProviderA and ProviderB). Some client code, call it ProviderLister would like to know what implementations are present and present them to the user for selection. The situation can be illustrated in the figure below.

How would one go about implementing this pattern? In this article, I will explore how one would go about doing this using Java EE CDI services.

Consider the implementation / component diagram below:

Here is the code that can implement this pattern.

The framework

The framework is in the spi jar file. The jar contains the following code in net.nihilanth.demo.spi.IProvider.java:

package net.nihilanth.demo.spi;

public interface IProvider {
    public String getName();
}

And that’s it. No other code, no dependencies.

The plugins

The plugins are also jar files. The plugin1 jar contains the following code in net.nihilanth.demo.plugin1.ProviderA.java:

package net.nihilanth.demo.plugin1;

import net.nihilanth.demo.spi.IProvider;

public class ProviderA implements IProvider {

    @Override
    public String getName() {
        return "ProviderA";
    }

}

The jar also contains a META-INF/beans.xml file which will tell our Java EE container to inspect the contents of the JAR for CDI injectable classes:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://java.sun.com/xml/ns/javaee"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/beans_1_0.xsd">
</beans>

As you can see, the file is actually empty apart from the appropriate namespace declarations. Its presence is enough to let CDI know that it should inspect our JAR and find injectable beans (ProviderA, in our case).

plugin2 contains very similar code, except that it provides net.nihilanth.demo.plugin2.ProviderB class.

Note that plugin1 and plugin2 jars will have a compile time dependency on the spi jar, since the ProviderX classes have a hard dependency on the IProvider interface, which is packaged in the spi jar.

The client code

Our client code is in the container war file. It contains a WEB-INF/beans.xml file with code similar to above to tell CDI to find injectable beans. Besides that, it contains our client code in net.nihilanth.demo.container.ProviderLister.java file, which is a JSF managed bean.

package net.nihilanth.demo.container;

import java.util.ArrayList;
import java.util.List;
import javax.enterprise.inject.Instance;
import javax.faces.bean.ManagedBean;
import javax.faces.bean.RequestScoped;
import javax.inject.Inject;
import net.nihilanth.demo.spi.IProvider;

@ManagedBean
@RequestScoped
public class ProviderLister {

    @Inject
    private Instance<IProvider> providerSource;

    public ProviderLister() {
    }

    public List<String> getProviderList() {
        List<String> names = new ArrayList<String>();

        for (IProvider provider : providerSource) {
            names.add(provider.getName());
        }

        return names;
    }
}

The important code here is:

    @Inject
    private Instance<IProvider> providerSource;

This is the line which tells CDI to inject an Instance object. We can the subsequently use this object to iterate over all implementations of IProvider implementations. Which implementations will be found? That depends on which implementations have been bundled in the war. I use maven to build my artifacts, so in my war’s pom.xml I include all the provider implementations I am interested in at runtime:

<project ...>
    <dependencies>
        <dependency>
            <groupId>net.nihilanth.demo</groupId>
            <artifactId>spi</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>net.nihilanth.demo</groupId>
            <artifactId>plugin1</artifactId>
            <version>1.0-SNAPSHOT</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>net.nihilanth.demo</groupId>
            <artifactId>plugin2</artifactId>
            <version>1.0-SNAPSHOT</version>
            <scope>runtime</scope>
        </dependency>        
    </dependencies>
</project>

The important thing to note is that the dependency on spi jar is a compile time dependency (the default in maven if you don’t override it with a scope directive), whereas the dependencies on the plugin jars are runtime dependencies: this essentially means that code in the war cannot directly refer to any code in the plugin jars and therefore cannot be coupled to them.

Now we just add a JSF page to list our discovered provider implementations (I am using the Facelets variant of JSF):

<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core">
    <h:head>
        <title>Facelet Title</title>
    </h:head>
    <h:body>
        Hello from Facelets
        <h:dataTable value="#{providerLister.providerList}" var="provider">
            <h:column>
                <f:facet name="header">Provider Name</f:facet>
                #{provider}
            </h:column>
        </h:dataTable>
    </h:body>
</html>

And that’s it! Now your war will be able to list IProvider implementations that were bundled with the war and show them. Of course, you still have to do something useful with them, and I hope to cover that in another post.

What’s going on here

What’s going on is that the Java EE CDI service has figured out that the client code wants to list all implementations of IProvider interface. It inspects the included jars for injectable beans (using the presence of beans.xml files to guide which jars to look into) and registers the found implementations internally. Then, on demand, it provides us with those implementations.

How to add more plugins

So, imaging in the future someone writes a ProviderC class. They should package it into a jar with a beans.xml file, and the author of war just needs to add this jar in the maven dependencies list in the pom.xml file of the war. The new class will be available at runtime.

Related stuff

In his excellent book Effective Java, Joshua Bloch discusses the service provider framework. In his framework, he does not assume the presence of anything like CDI. Therefore, his providers have to have code to register with the service provider framework somehow. I have shown that using CDI, we can get the container to do this registration for us.

Advertisements
Post a comment or leave a trackback: Trackback URL.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: