Sunday, April 15, 2012

Simple plugin architecture with java.util.ServiceLoader

Suppose you want to implement some kind of plug-in architecture. Java has simple mechanism for that. First of all, you have to think of an interface which will provide the functionality of a plugin, in Java terms it's called a service:
package com.example;

public interface CodecService {
    Encoder getEncoder(String charset);
    Decoder getDecoder(String charset);
}
This interface should somehow allow to discover if specific instance supports specific request. For example, it can return null if it doesn't. Next, you have to create concrete instances of the service, called service providers:
package com.example.jp;
// ...

public class JapaneseCodecServiceImpl implements CodecService {
    public Encoder getEncoder(String charset) {
        if (! "EUC-JP".equals(charset)) {
            // not supported
            return null;
        }
        return new JapaneseEncoder(charset);
    }
    // ...
}
Supposedly this class will reside in a different JAR than the service itself. Also, to enable automatic discovery of the service provider you'd have to create a file named META-INF/services/com.example.CodecService (obviously, the name will change from service to service). This file should contain one line for each provider with the provider fully qualified class name in it:
# comments are allowed
com.example.jp.JapaneseCodecServiceImpl
com.example.cyr.CyrillicCodecServiceImpl
A common practice is to bundle these files with the service providers into a single JAR. Now you have to create an access point to your service. This includes using java.util.ServiceLoader class. This class is parametrized with a service interface. You should create one single instance of that class for each service in your access point code:
public class CodecManager {
    private static ServiceLoader serviceLoader = ServiceLoader.load(CodecService.class);
    // ...   
}
After that, you can add a static method which will serve user's request using discovered service providers. This method will use ServiceLoader.iterator method (or make use of ServiceLoader being Iterable), which firstly iterates over cached providers and then, if you didn't stop iterating, will lazily load discovered providers one by one:
public class CodecManager {
    private static ServiceLoader serviceLoader = ServiceLoader.load(CodecService.class);
    
    public static Encoder getEncoder(String charset) {
        synchronized (serviceLoader) {
            for (CodecService cs : serviceLoader) {
                Encoder e = cs.getEncoder(charset);
                if (e != null) {
                    return e;
                }
            }
        }
    }

}
Note that, according to the documentation, ServiceLoader is not thread-safe. Although this same documentation shows an example without any thread safety means in place.

No comments: