Insel: Service-Factory, IoC, Lookup, Generics, Services und alles zusammen
Je größer eine Java-Anwendung wird, desto größer werden die Abhängigkeiten zwischen Klassen und Typen. Um die Abhängigkeiten zu reduzieren, ist zunächst gewünscht, sich nicht so sehr an Implementieren zu binden, sondern an Schnittstellen. (Das gelobte „Programmieren geben Schnittstellen und nicht gegen eine Implementierung.“) Eine Schnittstelle beschreibt dann Dienste, so genannte Services, auf die an anderer Stelle zurückgegriffen werden kann. Die nächste Frage ist, wie ein Service mit Geschäftslogik zu der Stelle kommt, an denen er benötigt wird, etwa auf der grafischen Oberfläche als Aktion hinter einer Schaltfläche. Hier haben sich zwei Wege herausgestellt:
- Service-Fabriken. Eine Service-Fabrik ist eine Zentrale, an die sich Interessenten wenden, wenn sie einen Service nutzen wollen. Die Fabrik liefert eine passende Implementierung, die immer eine Service-Schnittstelle implementiert. Welche Realisierung – also konkrete Klasse – die Fabrik liefert, soll den Nutzer nicht interessieren; eben Programmieren gegen Schnittstellen.
- Dependency Injection/Inversion of Control (IoC). Nach diesem Prinzip fragen die Interessenten nicht aktiv über eine zentrale Service-Fabrik nach den Diensten, sondern den Interessenten wird der Service über eine übergeordnete Einheit gegeben (injiziert). Die magische Einheit nennt sich IoC-Container. In der Vergangenheit hat sich das Spring-Framework als De-facto-Standard eines IoC-Containers herauskristallisiert.
Arbeiten mit dem ServiceLoader
Java SE bietet bisher keine Bibliothek für Dependency Injection aber mit der Klasse java.util.ServiceLoader eine einfache Realisierung für Service-Fabriken. Ein eigenes Programm soll auf einen Grüß-Dienst zurückgreifen, aber welche Implementierung das sein wird, soll an anderer Stelle entschieden werden.
ServiceLoader<Greeter> greeterServices = ServiceLoader.load( Greeter.class );
for ( Greeter greeter : greeterServices )
System.out.println( greeter.getClass() + " : " + greeter.greet( "Chris" ) );
ServiceLoader erfragt mit load() eine Realisierung, die die Schnittstelle Greeter implementieren soll. Die Realisierung ist der Service-Provider. Greeter deklariert eine greet()-Operation:
package com.tutego.insel.services;
public interface Greeter
{
String greet( String name );
}
Der Service liefert aber eine konkrete Klasse. Demnach muss es irgendwo eine Zuordnung geben, die einen Typnamen (Greeter) mit einer konkreten Klasse, der Service-Implementierung, verbindet. Dazu ist im Wurzelverzeichnis des Klassenpfades ein Order META-INF mit einem Unterordner services anzulegen. In diesem Unterordner ist eine Textdatei (provider-configuration file) zu setzen, die den gleichen Namen wie die Service-Schnittstelle besitzt:
META-INF/
services/
com.tutego.insel.services.Greeter
Diese Textdatei, die keine Dateiendung aufweist, enthält Zeilen mit voll qualifizierten Klassenamen (binary name genannt) für die Implementierung, die später hinter diesem Service stehen. Es kann eine Zeile oder durchaus mehrere Zeilen für unterschiedliche Implementierungen angegeben sein. In der Datei META-INF/services/com.tutego.insel.services.Greeter steht:
com.tutego.insel.services.FrisianGreeter
FrisianGreeter ist demnach unsere letzte Klasse und eine tatsächliche Implementierung des Services:
package com.tutego.insel.services;
public class FrisianGreeter implements Greeter
{
public String greet( String name ) {
return "Moin " + name + "!";
}
}
Utility-Klasse Lookup als ServiceLoader-Fassade
So nett der ServiceLoader auch ist, die API könnte ein wenig kürzer sein. Denn oftmals gibt es nur eine Service-Implementierung und nicht gleich mehrere. Daher soll eine Fassade eine knackigere API anbieten. Eine kurze Methode lookup() liefern genau den ersten Service (oder null) und lookupAll() gibt alle Service-Klassen in einer Sammlung zurück. (Das Listing nutzt mehrere Dinge, die die Insel bisher nicht vorgestellt hat! Dazu zählen Generics, Datenstrukturen, Iterator, Meta-Objekte.)
public class Lookup
{
public static <T> T lookup( Class<T> clazz )
{
Iterator<T> iterator = ServiceLoader.load( clazz ).iterator();
return iterator.hasNext() ? iterator.next() : null;
}
public static <T> Collection<? extends T> lookupAll( Class<T> clazz )
{
Collection<T> result = new ArrayList<T>();
for ( T e : ServiceLoader.load( clazz ) )
result.add( e );
return result;
}
}
Die Nutzung vereinfacht sich damit:
System.out.println( Lookup.lookup( Greeter.class ).greet( "Chris" ) ); // Moin Chris!
System.out.println( Lookup.lookupAll( Greeter.class ).size() ); // 1
Unverkennbar ist natürlich der Einfluss der NetBeans-Klasse http://bits.netbeans.org/dev/javadoc/org-openide-util/org/openide/util/Lookup.html.
Labels: Insel

1 Comments:
Wie sieht es denn mit Refactoring aus? Wenn ich den Namen des Paketes (oder den Namen des Interfaces über das Eclipse Refactoring "Move" ändere - wird die (nicht java) externe Datei nachgeführt?
Dies ist bei Spring Konfigurationsdateien über das Move möglich.
Ich frage, weil sich ja mit einer Packageänderung (oder einer Interfaceumbenennung) ja auch der Dateiname der externen Datei ändern muss (der Inhalt an sich ist wahrscheinlich auch über das normale Move oder Rename nachführbar).
Ist dies möglich, ist es natürlich auch interessant, den ServiceLoader zu verwenden.
Was allerdings ungünstig ist, ist, dass man sich durch den ServiceLoader ja wieder an eine "externe" Fabrik (bzw. durch den Lookup an eine eigene Fabrik) bindet. Dies versucht man ja durch Spring möglichst zu vermeiden.
Bei einer Applikation mit Spring würden nur einige Beans direkt aus der BeanFactory geladen (oder sogar automatisch injiziert), die abhängigen Beans durch Spring injiziert.
Beim ServiceLoader muss ich entsprechend für jedes Interface, für das ich eine Implementierung benötige, den ServiceLoader aufrufen. Also muss ich in jeder Klasse, die durch Interfaces entkoppelt wird, wieder eine Dependency zum ServiceLoader (oder seinem Helfer Lookup) aufbauen.
By
Florian, at März 17, 2008 9:52 AM
Kommentar veröffentlichen
<< Home