17.2 Wie eine Implementierung an die richtige Stelle kommt
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 Implementierungen zu binden, sondern an Schnittstellen. (Das ist das gelobte »Programmieren gegen Schnittstellen und nicht gegen eine Implementierung.«[106](Wenn etwa eine Klasse C eine Schnittstelle I implementiert und es werden nur Eigenschaften gebraucht, die I bereitstellt, sollte es zum Beispiel I ref = new C(); heißen statt C ref = new C();. Ein Methodenaufruf wie ref.m(); nennt sich dann »gegen« eine Schnittstelle durchgeführt.)) Eine Schnittstelle beschreibt dann Dienste, sogenannte Services, auf die an anderer Stelle zurückgegriffen werden kann.
Die nächste Frage ist, wie Services zusammenfinden. Nehmen wir an, es gibt eine Service-Realisierung für das Drucken und eine andere für die Aufbereitung eines Reports. Wenn wir nun eine Applikation schreiben wollen, die erst auf den Report-Service zugreift und anschließend diesen Report zum Drucker-Service gibt, muss die eigene Applikation irgendwie die Service-Implementierung finden beziehungsweise irgendwie die Service-Implementierung zur Applikation finden. Hier haben sich zwei Konzepte herauskristallisiert:
- Service-Fabriken: Eine Service-Fabrik ist eine Zentrale, an die sich Interessenten wenden, wenn sie einen Service nutzen wollen. Die Fabrik liefert ein passendes Objekt, das immer eine Service-Schnittstelle implementiert. Welche Realisierung – also konkrete Klasse – die Fabrik liefert, soll den Nutzer nicht interessieren; es geht eben um das 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. Es gibt für Injizierungen einen relativ neuen Standard, der im JSR-299 definiert wurde – er wird gleich noch kurz beschrieben. In der Vergangenheit hat sich das Spring-Framework als De-facto-Standard eines IoC-Containers herauskristallisiert. Mittlerweile wird es durch Guice oder auch durch JSR-299-Implementierungen wie die Java EE 6-Container ergänzt.
17.2.1 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.
Listing 17.13: com/tutego/insel/services/ServiceLoaderDemo, main()
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:
Listing 17.14: com/tutego/insel/services/Greeter.java
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 Ordner 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 Klassennamen (binary name genannt) für die Implementierung, die später hinter diesem Service steht. Es kann eine Zeile oder durchaus mehrere Zeilen für unterschiedliche Implementierungen angegeben sein:
Listing 17.15: META-INF/services/com.tutego.insel.services.Greeter
com.tutego.insel.services.FrisianGreeter
Abbildung 17.1: Vererbungsbeziehung zwischen FrisianGreeter und Greeter
FrisianGreeter ist demnach unsere letzte Klasse und eine tatsächliche Implementierung des Service:
Listing 17.16: com/tutego/insel/services/FrisianGreeter.java
package com.tutego.insel.services;
public class FrisianGreeter implements Greeter
{
@Override public String greet( String name )
{
return "Moin " + name + "!";
}
}
final class java.util.ServiceLoader<S> |
- static <S> ServiceLoader<S> load(Class<S> service)
Erzeugt einen neuen ServiceLoader. - Iterator<S> iterator()
Gibt nach und nach alle Services S vom ServiceLoader zurück.
17.2.2 Die 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() liefert 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 Datenstrukturen, der Iterator und Meta-Objekte.)
Listing 17.17: com/tutego/insel/services/Lookup.java, Lookup
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 wie folgt:
Listing 17.18: com/tutego/insel/services/LookupDemo.java, main()
String s = Lookup.lookup( Greeter.class ).greet( "Chris" );
System.out.println( s ); // Moin Chris!
System.out.println( Lookup.lookupAll( Greeter.class ).size() ); // 1
17.2.3 Contexts and Dependency Injection (CDI) aus dem JSR-299
Dependency Injection gibt es schon eine ganze Weile, aber erst spät hat sich mit dem JSR-299, »Contexts and Dependency Injection for the Java EE platform«, (kurz CDI) ein Standard herausgebildet. CDI ist Teil jedes Java EE 6-Containers und (bisher) kein Teil von Java SE 7. Dennoch lohnt es sich, diesen Standard etwas näher anzuschauen.
Die CDI-Referenzimplementierung Weld
Wir wollen für ein Beispiel die Referenzimplementierung Weld nutzen, die auch in der Referenzimplementierung GlassFish des Java EE-Standards genutzt wird. Von der Webseite http://seamframework.org/Weld/WeldDistributionDownloads laden wir das aktuelle Weld-Paket (etwa weld-1.1.2.Final.zip mit einer Größe von etwa 20 MiB), packen es aus und nehmen aus dem Verzeichnis artifacts\weld das Java-Archiv weld-se.jar in den Klassenpfad auf. Das war es schon.
CDI-Beispiel
Als Demonstrationsobjekt soll ein Configuration-Objekt in ein Application-Exemplar injiziert werden. Configuration soll globale Konfigurationsinformationen repräsentieren, auf die in der Regel andere Java-Objekte zurückgreifen. Anstatt hier selbst etwa eine Fabrik oder ein Singleton zu programmieren, soll der Container später das Objekt aufbauen und zu den interessierten Objekten bringen.
Listing 17.19: com/tutego/insel/cdi/Configuration.java, Configuration
public class Configuration
{
public String greetingMessage() { return "Willkommen Lena, ähh, CDI "; }
}
Die Klasse Application soll das Hauptprogramm bilden, das schon auf Configuration zurückgreifen möchte:
Listing 17.20: com/tutego/insel/cdi/Application.java, Application
import javax.inject.Inject;
public class Application
{
@Inject Configuration configuration;
public void start()
{
System.out.println( configuration.greetingMessage() );
}
}
Hier können wir die erste Annotation ablesen: @Inject. Dem Objekt Application soll ein Verweis auf das Configuration-Objekt in die Variable configuration injiziert werden. Es kann beliebig viele zu injizierende Attribute geben. Erst danach ist das Application-Objekt korrekt initialisiert. (Das ist wichtig zu verstehen, denn im Konstruktor ist der Zugriff auf die injizierten Objekte verboten, da das Application-Objekt natürlich zuerst über den Standard-Konstruktor erzeugt werden muss, bevor die Objektvariablen initialisiert werden können.) Die Methode start() ist eine selbstgewählte Methode, bei der das gesamte Programm starten soll. Hier lässt sich auf alle injizierten Attribute zurückgreifen, und das Programm testet dies mit einer kleinen Konsolenausgabe.
Damit ist unsere Applikation so weit fertig. Jetzt folgen noch zwei Schritte bis zum erfolgreichen Start:
- Wir setzen in unserem Projekt im src-Ordner ein Verzeichnis META-INF und legen dort eine leere Datei mit dem Namen beans.xml an. Sie ist dann nützlich, wenn nicht über Annotationen gearbeitet wird, sondern wenn die Beans extern beschrieben werden sollen. Da wir das nicht nutzen, kann die Datei leer bleiben.[107](Unsinnigerweise muss sie vorhanden sein, auch wenn sie leer ist – vielleicht ändert sich das aber in Zukunft.)
- Als Letztes muss die Applikation gestartet werden. Hier unterscheiden sich die CDI-Container ein wenig, und bei Weld gibt es verschiedene Varianten.[108](Dokumentiert unter http://docs.jboss.org/weld/reference/1.0.1-Final/en-US/html/environments.html#d0e5571.) Wir wählen eine Klasse mit einer statischen main()-Methode, die den Weld-Container initialisiert und dann die start()-Methode aufruft, um unser Programm zu starten.
Listing 17.21: com/tutego/insel/cdi/StartMain.java, StartMain, main()
public static void main( String[] args )
{
Application app = new Weld().initialize().instance().select( Application.class ).get();
app.start();
}
Wenn wir das Programm starten, kommen ein paar Logging-Meldungen, die wir jetzt ignorieren wollen, und dann erscheint die erwartete Ausgabe:
Willkommen Lena, ähh, CDI
Der CDI-Standard ist recht umfangreich, und die Weld-Dokumentation http://docs.jboss.org/weld/reference/latest/en-US/html/ gibt einen guten Einblick in die API. Kurz ein paar wichtige Fakten:
- Anstatt in Objektvariablen zu injizieren, kann der Container auch Setter aufrufen, die mit @Inject annotiert sind. Der Vorteil dabei ist, dass mit dem injizierten Wert gleich etwas gemacht werden kann. Beispielsweise können andere Zustände initialisiert werden.
- Neben der Injizierung in Attribute und dem Aufruf von Settern kann der Container auch einen Konstruktor aufrufen. Dann darf es aber nur einen parametrisierten Konstruktor mit dem zu injizierenden Objekt geben.
- Bei der Injizierung erstellt der Container immer ein neues Objekt. Im Fall von unserer Konfiguration muss das nicht sein. Die Annotation @Singleton an einer Klasse weist den Container an, nur einmal das Objekt aufzubauen und dann nur dieses eine Exemplar in alle Injektionspunkte zu spritzen.
Ihr Kommentar
Wie hat Ihnen das <openbook> gefallen? Wir freuen uns immer über Ihre freundlichen und kritischen Rückmeldungen.