Start - Publikationen - Wissen - TOGAF - Impressum -

Erste Ansätze


Prinzipiell ist auch im professionellen Umfeld Dependency Injection nicht vom Einsatz eines Frameworks abhängig. Es spricht nichts dagegen, sämtliche notwendige Dependency Injection in der main-Methode der Anwendung unterzubringen. Vielleicht haben Sie das intuitiv auch schon immer so gemacht. Um die Übersicht zu bewahren werden Sie jede Komponente Ihres Systems mit Initialisierungscode versehen, der dann in der main-Methode gerufen wird. Etwas komfortabler wird das Ganze, wenn Sie die zu initialisierenden Komponenten deklarieren, dann beim Start einlesen und auswerten.

Der beschriebene programmatische Ansatz löst jedes Problem der Dependency Injection. Der Nachteil eines solchen Vorgehens wird beim Deployment der Anwendung ersichtlich. Sie können nicht ohne Änderungen im Programmcode einzelne Komponente Ihrer Anwendung austauschen. Das wäre aber ein gewaltiger operativer Vorteil. Fehlerhafte Komponenten könnten dann zügig mit alten, stabilen Varianten oder mit Mock-Implementierungen getauscht werden. Um das zu erreichen muss Dependency Injection gewissermaßen 'plugabble' erfolgen. Aber auch jetzt kommen Sie noch immer ohne ein Framework gut zurecht. Bündeln Sie dazu die wichtigsten Eigenschaften der Komponenten Ihres Systems in Properties-Dateien, die im Classpath der Anwendung zu finden sind. In der main-Methode wird dann nach allen diesen Deklarationen gesucht und die Initialisierung durchgeführt.

Am besten wäre es, wenn man in der Initialisierungsphase der Anwendung den gesamten Classpath nach Implementierungen (service provider) von Interfaces (services) durchsucht und in einer Registrierung sammelt, die dann komfortabel ausgelesen werden kann! Brauchen Sie nicht bauen, gibt es schon:

Service Loading mit java.util.ServiceLoader


Dependency Injection ist seit dem JDK1.3 fester Bestandteil von Java, leider etwas versteckt in der JAR File Specification. Hier wird genormt, wie beim Starten einer Anwendung systematisch nach Implementierungen von Interfaces gesucht wird. Die Laufzeit sucht zu diesem Zweck nach Dateien innerhalb der META-INF/services Verzeichnisse im gesamten Classpath. Der Name dieser Dateien entspricht dabei dem Namen des Interfaces. In der Datei wird jede nicht leere Zeile die kein Kommentar ist als Klassenname einer Implementierung interpretiert. Ein Beispiel: angenommen es soll für das Interface com.a.b.IService eine Implementiertung com.test.ServiceImpl bereitgestellt werden, dann stellt man im Classpath der Anwendung diese Struktur (zum Beispiel als JAR) zur Verfügung:

com/test/ServiceImpl.class
:
META-INF/services/com.a.b.IService
Der Inhalt der Datei META-INF/services/com.a.b.IService ist schlicht
 # Datei: META-INF/services/com.a.b.IService
 # alle Implementierungen von com.a.b.IService
 com.test.ServiceImpl
 :
Instanzen der so deklarierten Implementierungsklassen können dann über den ServiceLoader angesprochen werden. Jede mit ServiceLoader.load(IService.class) erzeugte Instanz hält im Kontext des beteiligten ClassLoaders einen Cache der gefundenen und instanziierten Service-Implementierungen. serviceLoader.iterator() kann dann beliebig oft gerufen werden, erst das iterator.next() erzeugt die Instanz der Serviceimplementierung oder, wenn vorhanden, nimmt die aus dem Cache. ServiceLoader-Instanzen sind damit im Kontext des ClassLoaders eine Singleton-Factory für Serviceimplementierung:
ServiceLoader<IService> sl = ServiceLoader.load(IService.class);
Iterator<IService> iter = sl.iterator(); 
while (iter.hasNext()) {
  IService serviceImpl = iter.next();
  ...
}
Wird kein ClassLoader explizit vorgegeben (wie im Beispiel) wird der ContextClassLoader des beteiligten Threads benutzt. Im Allgemeinen wird man solchen Code demnach ausschließlich und einmalig im Rahmen der Initialisierung der Anwendung unterbringen.

Service Loading war immerhin schon vor dem Start des Dependency Injection Hype verfügbar. Leider ist es kaum bekannt und über die Gründe kann ich nur spekulieren:

  • Service Loading hat den "falschen" Namen, das ist nicht verwunderlich, da es älter ist als der Dependency Injection Begriff
  • die Service Loading API ist in der JAR Spezifikation versteckt
  • die weitreichenden positiven Konsequenzen sind von den Urhebern nie diskutiert worden
Ein weiterer Grund ist sicher die mehrfache Änderung der API zum Service Loading:
JRE Service Loader
1.3 sun.misc.Service
1.4,1.5 javax.imageio.spi.ServiceRegistry
ab 1.6 java.util.ServiceLoader (siehe Beispiel)
Die nächste Inkarnation des Service Loading als waschechte Dependency Injection steht mit Contexts and Dependency Injection for Java EE ins Haus.

Ansonsten leistet Service Loading perfekt und voll integriert genau das, was man braucht, nicht mehr und nicht weniger. Da es Bestandteil des JRE ist, betrachte ich es immer noch als Methode der Wahl.

Service Loading in Java EE Umgebungen


Service Loading kommt ohne synchronisierten Code aus und jeder Aufruf von ServiceLoader.load() erzeugt jeweils eine neue ServiceLoader Instanz (welche immer neue Service Instanzen liefert). Wenn das nicht gewünscht ist, muss man sicherstellen, dass der Aufruf von ServiceLoader.load() nur einmal erfolgt, dann sind auch die gelieferten Service Intanzen identisch.

Im Webcontainer ist jedes Servlet ein Container-Singleton. Somit ruft man ServiceLoader.load() einfach ausschließlich während der Initialisierung des Servlets und seiner Hilfsklassen. Jede Serviceimplementierung wird damit genau einmal instanziiert. Auf diese Weise kann man sehr generische Webanwendungen implementieren: Filter, Listener und sogar die Abarbeitung von Requests kann man an entsprechende Service-Location Implementierungen binden, die man einfach in das WEB-INF/lib Verzeichnis legt.

Auch im EJB Container kann Service Loading ebenfalls im Rahmen der Bean-Initialisierung eingesetzt werden, da in der Service Loader Implementierung auf synchronisierten Code verzichtet wurde. Die Archive mit den Service-Implementierungen legt man im META-INF/lib Verzeichnis des EAR. Zu beachten ist, dass per Bean-Instanz eigene Serviceimplementierung instanziiert werden. Üblicherweise wird man also pro Bean eine ServiceLoader-Instanz erzeugen und wiederverwenden. Die Serviceimplementierungen müssen natürlich, wie jeder Hilfscode einer Bean, den Regeln der EJB Programmierung gehorchen. Insbesondere

  • ist auf synchronierten Code zu verzichten
  • dürfen statische Felder nicht, oder nur mit read-only Semantik verwendet werden
  • sind Ressourcen der Umgebung ausschließlich über den EJB Container zu akquirieren

Frameworks


Wer seine Kollegen wirklich beeindrucken will nutzt für Dependency Injection natürlich eines der vielen Frameworks für diesen Zweck. Allerdings haben manche Frameworks aufgrund ihrer hohen Einstiegsschwelle ein Akzeptanzproblem. Ohne Anspruch auf Vollständigkeit (Seam, HiveMind, Excalibur/Avalon und viele mehr fehlen hier) werden hier die wichtigsten besprochen.

Eclipse Runtime Die Eclipse Runtime, also der Kern der OSGi Laufzeit bei Eclipse, ist vielleicht nicht auf dem ersten Blick ein Dependency Injection Framework. Aber ersetzen Sie einmal die Begriffe plugin mit 'Komponente', extension points mit 'Interface' und extension mit 'konkrete Implementierung des Interface', dann sehen Sie die Parallelen. Bei der Eclipse Runtime werden alle Abhängigkeiten zwischen den Komponenten (plugins) in Form von extension points deklariert und damit über ein (aus Programmierer-Sicht) außersprachlichen Mechanismus gekapselt. Die API der plugins ist somit getrennt vom Code beschrieben - als plugin.xml. Dieser ganze Aufwand lohnt, wenn Sie eine sehr große Anwendung in einem sehr großen, heterogenen Team entwicklen. Die Eclipse IDE ist das beste Beispiel für ein solches Projekt.

EJB3.0 wird hier der Vollständigkeit wegen aufgeführt. Dependency Injection kann basierend auf Annotations für einige Ressourcen vom Container ausgeführt werden. Gleichzeitig entfällt der JNDI Lookup der betreffenden Ressource im Code. Hier eine Übersicht, welche Klassen mit welchen Ressourcen versorgt werden können:

Schicht Klassen Ressourcen
Web Servlets, Listeners, Webservice end-points, JAX-RPC handler DataSource, JMS, Mail, EJB, Environment entries, EntityManager, UserTransaction
EJB Beans, Interceptors, Webservices end-points DataSource, JMS, Mail, Environment entries, EntityManager, EJB Context, UserTransaction, TimerService
Unterstützt wird field injection und setter injection. Dazu sind die entsprechenden Felder repsektive Setter mit der Annotation @Resource zu versehen:
// field injection
@Resource(name="jdbc/SomeDataSource") 
private javax.sql.DataSource dataSource;
// 
// setter injection
@Resource(name="jdbc/SomeDataSource") 
private void setSomeDataSource(DataSource dataSource) { 
  this.someDataSource = dataSource; 
}
Leider kann für Java EE 5 Dependency Injection nicht auf eigene Helperklassen erweitert werden.

Spring: Spring kommt als Gemischtwarenladen daher und bietet anscheinend Lösungen für alles im Zusammenhang mit Java Software. Spring ist exzellent vermarktet und es gab Zeiten, da galt für viele: Dependency Injection IST Spring. Tatsächlich ist die Basis von Spring (auf die wir hier fokuszieren) ein Framework für deklarative Dependency Injection, leistet aber nicht die plugabble Dependency Injection wie im Artikel beschrieben. Wenn Sie von Spring nur diese Kernfunktionalität nutzen wollen rate ich von der Nutzung von Spring ab.

PicoContainer: Das grundlegende Arbeitsprinzip von PicoContainer wird am Beispiel der constructor injection beschrieben. Dabei geht man davon aus, dass die konkreten Implementierungen der Interfaces einer Komponente in den Konstruktoren ihrer Klassen übergeben werden. Der PicoContainer kann dann Instanzen von Klassen erzeugen, indem man an einer zentralen Stelle die konkreten Implementierungen der Konstruktor-Argumente deklariert. Hier ein Beispiel. Die Klasse A kann instanziiert werden, wenn im Konstruktor eine Instanz des Interfaces IA1 und IA2 vorgegeben wird. Nehmen wir an B implementiert IA1 und C implementiert IA2, der Konstruktor von B ist parameterlos und der Konstruktor von C braucht eine String-Konstante, dann ließe sich im Pseudocode eine Instanz von A so erzeugen:

A(IA1, IA2) = new A(new B(), new C('x'))

Im PicoContainer hingegen deklariert man solche Abhängigkeiten an zentraler Stelle und lässt sich Instanzen vom Container erzeugen:

// Registrierung
container.register(A.class, B.class, C.class);
container.register(C.class, 'x');
:
// im Code:
A(IA1, IA2) = container.getInstance(A.class);
Guice: google-Guice ist nach meiner Einschätzung das eleganteste Framework für diesen Zweck und zeichnet sich (für die Grundfunktionen) durch eine flache Lernkurve aus. Das Binden einer Implementierung an ein Interface erfolgt in sogenannten Modulen
:
bind(Service.class).to(ServiceImpl.class).in(Scope.SINGLETON);
:
Dieses Binding wird dann auf passende Methoden mit der Annotation @Inject angewendet
class Client {
  @Inject
  void injectService(Service service) {
    :
  }
}
Damit @Inject überhaupt berücksichtigt wird, werden Instanzen von Client über einen Injector besorgt
Injector inj = Guice.createInjector();
Client client = inj.createInstance(Client.class);
:
copyright © 2003-2021 | Dr. Christian Dürr | prozesse-und-systeme.de | all rights reserved