Abhängigkeiten vermeiden mit der Java Service Provider Spezifikation

Durch ein aktuelles Projekt der Cassini Consulting konnte ich mich endlich Mal tiefer mit einem grundsätzlichen Problem beschäftigen: der losen Kopplung eines Service Provider Interfaces (SPI) oder grundsätzlich eines API und seiner Implementierungen in Form von Plugins. Das Erstaunliche daran (überlagert von Themen wie Dependency Injection und Spring): Es gibt bereits seit dem JDK 1.3 eine sehr einfache Lösung für dieses Problem und sie funktioniert auch ab Werk: Die Java Service Provider Spezifikation.

Die Problemstellung lässt sich am besten in Form eines Beispiels erläutern. Die Projektleitung kommt auf die irsinnige Idee, ein Quoter-Interface als Teil einer API zu definieren, über welches Texte in Anführungszeichen gefasst werden können. Etwa so:

package demo.api;

public interface IQuoter {
        String quote(String s);
}

Dazu steuern die Entwickler eine Implementierung für den deutschen Sprachraum bei, welcher tief- und hochgestellte Anführungszeichen verwendet (über Unicode):

package demo.impl;

public class GermanQuoter implements IQuoter {
        public String quote(String s) {
                return '\u201E' + s + '\u201D';
        }
}

Die Frage ist nun, wie kann man diese Implementierung verwenden,

  • ohne sie direkt in der Applikation zu importieren und
  • ohne sie direkt in der API zu referenzieren?

Die Lösung ist interessanterweise bereits seit dem JDK 1.3 Teil der Java Standard Edition (SE), auch wenn sie zwischen den Versionen einige Male den Platz gewechselt hat. Mit Java 6 scheint sie nun aber ihren endgültigen Platz gefunden zu haben als java.util.ServiceLoader.

Dafür muß die Implementierung als eigenes JAR ausgeliefert werden, denn folgende Punkte sind Voraussetzung für die Nutzung des ServiceLoader:

  1. im JAR muß ein Verzeichnis META-INF/services vorhanden sein
  2. in diesem Verzeichnis muß für jede API Klasse (Interface oder abstrakte Klasse) eine Datei gleichen Namens liegen
  3. in jeder dieser Dateien muß der vollqualifizierte Namen der Implementations-Klasse vermerkt sein

Für unser Beispiel bedeutet das also, daß der Inhalt des Implementierungs-JAR wie folgt aussieht:
demo.impl.GermanQuoter
META-INF/services/demo.api.IQuoter

Und die einzige Zeile in der Datei demo.api.IQuoter liest sich wie folgt:
demo.impl.GermanQuoter

Wie kommt nun unsere Applikation, welche die API und das Implementierungs-JAR im Klassenpfad hat an die Implementierung? Nun, über Aufruf der entsprechenden Service Locator, welche sich je nach JDK an verschiedenen Stellen finden:

Also schreiben wir uns für das JDK 1.6 eine kleine, generische Utility-Klasse, welche eine oder alle Implementierungen für eine API-Artefakt liefern kann:

package demo.util;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.ServiceLoader;

public final class GenericServiceLocator {

    private GenericServiceLocator() {
    }

    public static <T> T locate(final Class<T> clazz) {
        final List services = locateAll(clazz);
        return services.isEmpty() ? (T) null : services.get(0);
    }

    public static <T> List<T> locateAll(final Class<T> clazz) {

        final Iterator<T> iterator = ServiceLoader.load(clazz).iterator();
        final List<T> services = new ArrayList<T>();

        while(iterator.hasNext()) {
            try {
                services.add(iterator.next());
            } catch (Error e) {
                e.printStackTrace(System.err);
            }
        }

        return services;

    }
}

Und so sieht dann der Aufruf aus unserer Beispiel-Applikation aus:

package demo;

import demo.api.IQuoter;
import demo.util.GenericServiceLocator;

public class App {
    public static void main( String[] args ) {
        System.out.println(
                GenericServiceLocator
                    .locate(IQuoter.class)
                    .quote("Quote Me")
        );
    }
}

Das ist alles. Befinden sich API, Implementierung und Applikation im Klassenpfad, kann der Test über den Aufruf von
java -cp demo-api.jar;demo-impl.jar;demo-app.jar demo.App
erfolgen. Das Ergebnis sollte auf Unicode-Terminals ein „Quote Me” sein. Auf der Windows XP Console ist es übrigens äQuote Meö

Zum Ausprobieren ist das Beispiel-Projekt als Maven2 Multi-Module beigefügt, die Verwendung in eigenen Projekten ist – selbstverständlich – gestattet.

  1. zeeman sagt:

    Kurz & Knackig.
    Sehr gut. Danke 🙂