Loading Classes at Runtime Using ServiceLoader

Sometimes, you just wont know what class you need to use until runtime. So how can you devise a decent strategy for finding and loading the appropriate class? There are different approaches to how you can determine and use different implementations at runtime, but when you need it to be generally extensible, this can be a tedious process.

Suppose you’re creating an extensible connection/protocol framework. You want to be able to have some static protocol provider that will create a connection based on the protocol that will be used. Now, you don’t know what protocol will be used until runtime and you need it to be generally expandable to add more protocols later. Starting with the abstract Connection class, let’s look at an example.

import protocol.Connection;
import protocol.NoSuchProtocolException;
import protocol.ProtocolProvider;

public class Main {

   public static void main(String[] args) {
      Connection c;
      try {
         c = ProtocolProvider.getConnection("http");
         System.out.println(c.getData());
      } catch (NoSuchProtocolException e) {
         System.out.println(e.getMessage());
      }
   }
}

Of course, this example is by no means complete, but it does show enough for the purposes of this post. We want to be able to ask the ProtocolProvider for some connection that supports the given http protocol. It’s up to the ProtocolProvider to know about all the protocols that are supported, and return the instance of a Connection that supports it. Let’s look at one way we can accomplish this.

package protocol;

import java.util.HashMap;
import java.util.Map;

public class StaticProtocolProvider {
   private static Map<String, Connection> protocols;
   static {
      protocols = new HashMap<String, Connection>();
      protocols.put("http", new HTTPConnection());
   }

   public static Connection getConnection(String protocol)
                                                throws NoSuchProtocolException {
      if (protocols.containsKey(protocol))
         return protocols.get(protocol);
      throw new NoSuchProtocolException("No registered protocol: " + protocol);
   }
}

In this class, there is a static block that will create an instance of each sub-class of Connection and add it to the protocol map. This is an interesting way to solve the problem. It ensures that you know about all the protocols, but it means that you need to enumerate them all and I’m sure you can imagine why that would be inconvenient. When we want to add support for a new protocol, we not only have to create a new sub-class of Connection, but we also have to put an entry in this static block. Maybe this is behavior we want, maybe not.

Java provides a ServiceProvider class that can be used to load classes at runtime. This is done by looking in the META-INF directory and loading the appropriate service class. In this example, the service class in question is the Connection class and the provider class(s) would be any class that sub-classes Connection (i.e. HTTPConnection). The ServiceLoader works by looking in the META-INF/services directory for a file with the name of the fully qualified class path for the given service and loading classes described inside. Files in the services directory contain a list of class paths to the providers. In our example there is a line inside the protocol.Connection file that says: “protocol.HTTPConnection”.

Now, we can use the ServiceLoader to load a provider at runtime when we need.

package protocol;

import java.util.ServiceLoader;

public class ProtocolProvider {
   private static ServiceLoader<Connection> protocolLoader;

   public static Connection getConnection(String protocol)
                                                throws NoSuchProtocolException {
      if(protocolLoader == null)
         protocolLoader = ServiceLoader.load(Connection.class);
      for (Connection connection : protocolLoader) {
         if (protocol.equals(connection.getProtocol()))
            return connection;
      }
      throw new NoSuchProtocolException("No registered protocol: " + protocol);
   }
}

Now, the downside to this approach is having to write to the services file every time you add a new class. You might think that this is no better then using the static block described in the first example. On this surface, this is true. My solution was to use a project called SPI. This provides the ability to annotate your class so it can auto-generate the appropriate services file. For example, the HTTPConnection class would look like this (notice the annotation on the class):

package protocol;

import org.mangosdk.spi.ProviderFor;

@ProviderFor(Connection.class)
public class HTTPConnection extends Connection {

   public HTTPConnection() {
      super("http");
   }

   @Override
   public String getData() {
      return "[OK]";
   }
}

Now, whenever a new class is written, it only needs to have this annotation added to it. This provides a much more flexible solution then the static block.

You can get the source to this example here (note: it will require you to use SPI)

Cheers

Comments

Add Your Comment

  Textile help