Schnelle DI für Java und Android

Dependency Injection mit Dagger

Sven Ruppert
© shutterstock.com/Sergey856

Dagger ist ein quelloffenes Dependency-Injection-(DI-)Framework, das auf Guice basiert. Allerdings waren die Entwickler von Dagger nicht mit dem Grundprinzip von Guice zufrieden: Immer wieder hatten sie in größeren Projekten Unmengen an Binding-Code zu schreiben. Da dies aber Teil der statischen Semantik ist, verfolgen sie die Strategie, dieses Binding zu extrahieren und in Form von Quelltext abzulegen. Ein weiteres Entwurfsziel ist die gute Lesbarkeit und Unterstützung beim Debugging-Prozess. Im Folgenden werden wir uns Dagger genauer ansehen und die ersten Schritte mit dem Framework gehen. Wir beschäftigen uns hier mit Dagger ab der Version 1.2.

Eine der Besonderheiten von Dagger ist dessen Verfügbarkeit in Java- und Android-Systemen. Sehen wir uns ein einfaches Beispiel an und beginnen mit einem simplen @Inject einer Instanz (MainService) in unsere Main-Klasse: 

public class Main {
  @Inject MainService mainService;
  /..
}
public interface MainService {
  public String execute(String txt);
}
public class MainServiceImpl implements MainService {
  public String execute(String txt){
    return "MainServiceImpl _ " + txt;
  }
}

Der erste Schritt, um eine Instanz einer Klasse mit injizierten Attributen zu erhalten, ist die Verwendung des Objekt-Graphen. Der ObjectGraph ist die Repräsentation aller Abhängigkeiten, die für eine Instanziierung notwendig sind. Im folgenden Listing ist die Initialisierung und Anwendung von Dagger in der Klasse Main zu sehen.

public class Main {
 @Inject MainService mainService;

  public static void main(String[] args) {
    //bootstrapping
    ObjectGraph objectGraph 
        = ObjectGraph.create(new BusinessModule());
    Main main = objectGraph.get(Main.class);
//    Main main = new Main();
//    objectGraph.inject(main);
    System.out.println("execute = " + main.mainService.execute("Und Los"));
    System.out.println("execute = " + main.mainService.execute("Und Los"));
    System.out.println("execute = " + main.mainService.execute("Und Los"));
  }
}

Es gibt bei Dagger zwei Wege, um an eine Instanz einer Klasse zu gelangen, die injizierte Attribute enthält. Der eine Weg geht über die Methode get(..) der Klasse ObjectGraph und liefert direkt eine fertige Instanz. Der andere Weg geht über den expliziten Aufruf der Methode inject(..) und kann auf bestehende Instanzen angewendet werden. Damit kann man den Zeitpunkt zwischen der Erzeugung der haltenden Klasseninstanz und den Membern selbst kontrollieren.

Es gibt noch eine Kleinigkeit zu beachten: Zur Erzeugung einer Instanz der Klasse ObjectGraph wird als Parameter der Methode create(..) noch ein Modul benötigt. In unserem Fall new BusinessModule(). In diesem Modul werden die Abhängigkeiten untereinander definiert; ebenso die Producer, die verwendet werden sollen.

@Module(library = false, injects = {Main.class}, complete = true)
public class BusinessModule {
  @Provides
  MainService provideMainService() {
    return new MainServiceImpl();
  }
}

In dem Producer kann entweder die Instanz selbst erzeugt werden, oder man kann Injection über Methodenparameter verwenden (nachfolgendes Listing). Hier ist Folgendes zu beachten: Bei Dagger muss man entweder den new-Konstruktor verwenden – wenn die zu erzeugende Instanz selber keine Attribute hat, die injiziert werden – oder eine Instanz per Methodenparameter geben lassen, wenn diese Instanz selbst Attribute injiziert bekommt.  

@Provides
MainService provideMainService(MainServiceImpl mainService) {
  return mainService;
}

Interne Funktionsweise von Dagger

Die grundlegende Frage, die sich einem Entwickler recht schnell stellt, ist diejenige der Definition der Abhängigkeiten. Es gibt in Java verschiedene Wege und Zeitpunkte, um die Abhängigkeiten zu definieren. Handelt es sich um eindeutige Abhängigkeiten, ist es meist einfach. Gibt es Fallunterscheidungen oder Multiplizitäten, wird es schon komplizierter.

Erweitern wir unser Beispiel um ein Interface SubService. Für dieses Interface gibt es zwei Implementierungen: SubServiceA und SubServiceB. Wenn man nun im Quelltext ein @Inject SubService verwendet, ist es nicht mehr eindeutig, welche Implementierung gemeint ist. Gehen wir im Folgenden davon aus, dass es zur Erzeugung einer Instanz immer eine korrespondierende Methode provideXXX (in unserem Beispiel provideMainService()) gibt, die mit der Annotation @Produces versehen worden ist. Es handelt sich hierbei um eine Factory-Methode. 

public class MainServiceImpl implements MainService {
  @Inject SubService subService;
  public String execute(String txt){
    return subService.work(txt);
  }
}

Ebenfalls wird eine Factory-Methode für die Erzeugung der Instanz von SubService benötigt. Hier kann man sich für eine Implementierung (SubServiceA oder SubServiceB) entscheiden, die verwendet werden soll. Das ist natürlich eine statische Entscheidung. Zu einem späteren Zeitpunkt werden wir dies dynamisch gestalten. 

@Provides
SubService provideSubService() {
  return new SubServiceA();
}

Wie aber erkennt Dagger, welche Instanz nun wo verwendet werden soll? Reflection oder Konfiguration ist hier die Antwort. In manchen Frameworks wird der Ansatz der Konfiguration verwendet. Diese Konfiguration wird dann ausgelesen und als Basis für die Instanziierung verwendet. Solche Ansätze haben leider immer das Problem, dass sie umständlich und fehleranfällig sind.

Die Alternative: Reflection. Zur Laufzeit kann ermittelt werden, welche Klasse welche Abhängigkeiten hat. Die Auflösung der Multiplizitäten ist damit noch nicht gelöst. JSR-330 hat hierfür den Qualifier @Named(..) vorgesehen. Damit kann man den jeweiligen Implementierungen Namen geben, die dann bei der Auflösung verwendet werden. Geben wir der Implementierung SubServiceA ein @Named(„A“) und der Implementierung SubServiceB ein @Named(„B“) mit. Nun muss man an der Stelle, an der ein @Inject definiert wird, zusätzlich den Qualifier angeben, z. B.

@Inject @Named(„A“) SubService subservice;

Die dazugehörige Factory-Methode wird ebenfalls mit @Named(„A“) versehen. Und genau über die Kombination Klasse/Interface und Annotation findet dann das Matching statt.

Damit man nun wieder ein einfaches

@Inject SubService subservice

verwenden kann, muss man noch eine Factory-Methode ohne Qualifier schreiben.

@Module(library = false, injects = {Main.class},complete = true)
public class BusinessModule {
  @Provides
  MainService provideMainService(MainServiceImpl mainService) {
    return mainService;
  }
  @Provides @Named("A")
  SubService provideSubServiceA() {
    return new SubServiceA();
  }
  @Provides @Named("B")
  SubService provideSubServiceB() {
    return new SubServiceB();
  }
  @Provides
  SubService provideSubService(@Named("A") SubService subServiceA) {
    return subServiceA;
  }
}

Wie bzw. wann werden nun die Abhängigkeiten erkannt? Das kann zur Laufzeit mittels Reflection realisiert werden. Dagger geht einen etwas anderen Weg: Reflection wird zwar verwendet, aber nicht zur Laufzeit. Es wird in den compile-Prozess eingegriffen. Die Abhängigkeiten werden mithilfe von Reflection aufgelöst und auch überprüft. Dieser Prozess erzeugt verschiedene Ergebnisse. Zum einen wird eine Datei mit der Endung dot angelegt. Dort kann man die gefunden Abhängigkeiten nachlesen. In unserem Fall die folgenden:

digraph G1 {
  concentrate = true;
  Main -> MainService;
  MainService -> MainServiceImpl;
  MainServiceImpl -> SubService;
  n2 [label="@javax.inject.Named(value=A)
           /org.rapidpm.demo.dagger.business.subservice.SubService"];
  SubService -> n2;
}

Die Verbindungen sind leicht zu erkennen. Aber was bedeuten die Klassen, die generiert werden? Gemeint sind: Main$$InjectAdapter , MainServiceImpl$$InjectAdapter und BusinessModule$$ModuleAdapter.

Beginnen wir mit der Klasse Main$$InjectAdapter. Der Adapter bietet eine Methode get(), die eine fertig instanziierte Referenz zurückgibt. Hier kann man den Lifecycle genau sehen – und der ist recht übersichtlich. Er besteht genau aus zwei Schritten: Erstens wird die Instanz erzeugt, zweitens die Methode injectMembers(..) aufgerufen. Außerdem ist dort die Verbindung zu den anderen Klassen abgebildet. Damit ist alles in der statischen Semantik, und zur Laufzeit wird ein entsprechender Getter aufgerufen. Dieser Ansatz ist zur Laufzeit natürlich wesentlich performanter als die Verwendung von Reflection. Diese Adapter-Klassen bilden also das vollständige Binding inklusive aller Kombinationen bzw. Varianten der Applikationen ab.

Aufmacherbild: Ancient sword with the bronze handholdon a beautiful background von shutterstock.com / Urheberrecht: Sergey856

 [ header = Praktischer Einsatz, Besonderheiten von Dagger ]

Praktischer Einsatz von Dagger

Nun sehen wir uns an, wie man sich zur Laufzeit für eine Implementierung entscheiden kann. Wir verwenden hier den Ansatz eines Context-Objekts. In diesem Fall ist es ein Singleton, in dem ein Attribut vom Typ boolean gehalten wird. In Abhängigkeit vom Wert wird in der Produces-Methode die eine oder andere Implementierung verwendet.

@Provides
SubService provideSubService(@Named("A") SubService subServiceA, 
                             @Named("B") SubService subServiceB) {
  if(Context.getInstance().defaultImpl){
    return subServiceA;
  } else{
    return subServiceB;
  }
}
//Ausgabe
SubServiceA = 2014-11-21T14:15:56.364
SubSubServiceImpl => 2014-11-21T13:15:56.396Z
SubServiceB = 2014-11-21T14:15:56.445
SubSubServiceImpl => 2014-11-21T13:15:56.445Z
execute = Und Los_A_SubSubServiceImpl Und Los
execute = Und Los_A_SubSubServiceImpl Und Los
execute = Und Los_A_SubSubServiceImpl Und Los

Der Nachteil hier ist die gleichzeitige Instanziierung der beiden Implementierungen, obwohl nur eine benötigt wird. Ebenfalls ist zu sehen, dass die Instanzen immer von außen nach innen instanziiert werden. In unserem Fall wird erst der SubService, dann der SubSubService erzeugt.

Lazy<T>, der virtuelle Proxy

Man kann hier auch auf die Implementierung des Virtual Proxy zurückgreifen. In dem Fall wird eine Instanz erst bei Zugriff auf den Proxy angelegt. Danach jedoch liefert der Proxy immer dieselbe Instanz zurück. Es wird also nur der erste Zugriff zeitlich so lange verzögert, bis Bedarf besteht. 

@Provides
SubService provideSubService(@Named("A") Lazy subServiceA, 
                             @Named("B") Lazy subServiceB) {
  if (Context.getInstance().defaultImpl) {
    return subServiceA.get();
  } else {
    return subServiceB.get();
  }
}

SubServiceA = 2014-11-21T14:26:44.567
SubSubServiceImpl => 2014-11-21T13:26:44.588Z
execute = Und Los_A_SubSubServiceImpl Und Los
execute = Und Los_A_SubSubServiceImpl Und Los
execute = Und Los_A_SubSubServiceImpl Und Los

Provider<T>, die Fabrik

Manchmal möchte man die Fabrik bzw. den Provider selbst zur Verfügung haben, um beliebig viele Instanzen programmatisch erzeugen zu können. Hierfür existiert die Klasse Provider. Auch hier handelt es sich wieder um einen Proxy, der mittels Aufruf der Methode get() veranlasst wird, eine neue Instanz zu erzeugen.

public class Main {
  @Inject Provider mainService;
  public static void main(String[] args) {
    final ObjectGraph objectGraph = ObjectGraph.create(new BusinessModule());
    final Main main = objectGraph.get(Main.class);
    System.out.println("execute = " 
        + main.mainService.get().execute("Und Los"));
    System.out.println("execute = " 
        + main.mainService.get().execute("Und Los"));
    System.out.println("execute = " 
        + main.mainService.get().execute("Und Los"));
  }
}
SubServiceA = 2014-11-21T14:39:47.146
SubSubServiceImpl => 2014-11-21T13:39:47.161Z
execute = Und Los_A_SubSubServiceImpl Und Los
SubServiceA = 2014-11-21T14:39:47.178
SubSubServiceImpl => 2014-11-21T13:39:47.178Z
execute = Und Los_A_SubSubServiceImpl Und Los
SubServiceA = 2014-11-21T14:39:47.179
SubSubServiceImpl => 2014-11-21T13:39:47.179Z
execute = Und Los_A_SubSubServiceImpl Und Los

Modularer Aufbau

Bisher haben wir in einer Modulklasse alle erzeugenden Methoden zusammengefasst. Allerdings benötigt man bei größeren Implementierungen sicherlich eine Menge dieser Methoden. Um diese Methoden auf verschiedene Module zu verteilen, kann man in dem jeweiligen Obermodul die benötigten Submodule mit einbinden. Das passiert in der Klassenannotation @Module.  

@Module(
    injects = {Main.class},
    includes = SubServiceModule.class
    )
public class BusinessModule {
  @Provides
  MainService provideMainService(MainServiceImpl mainService) {
    return mainService;
  }
}

Dieser Qualifier hat noch mehr mögliche Attribute. Hier allerdings benötigen wir erst einmal nur das Attribut includes. Dabei kann man alle Submodule angeben, die benötigt werden. Das Attribut injects gibt an, für welche Klasse dieses Modul zuständig ist – in unserem Fall für die Klasse Main, da hier der initiale @Inject stattfindet. Der rekursive Abstieg selbst muss dann nicht mehr angegeben werden.

Nun sind wir in der Lage, einzelne Module zu erzeugen und beliebig zusammenzubauen. Zusätzlich lässt sich der Zeitpunkt beeinflussen, zu dem die jeweiligen Objekte erzeugt werden. Das Umschalten der Implementierung funktioniert über die aktive Auswahl aufgrund von Entscheidungskriterien in dem jeweiligen Provider. Wichtig zu wissen ist, dass eine Instanz der Modul-Klasse selbst keine @Inject-Anweisungen verarbeitet. 

public class Main {
  @Inject Provider mainService;
  public static void main(String[] args) {
    final ObjectGraph objectGraph = ObjectGraph.create(new BusinessModule());
    final Main main = objectGraph.get(Main.class);
    System.out.println("execute = " 
	  + main.mainService.get().execute("Und Los"));
    Context.getInstance().defaultImpl = false;
    System.out.println("execute = " 
	  + main.mainService.get().execute("Und Los"));
    Context.getInstance().defaultImpl = true;
    System.out.println("execute = " 
	  + main.mainService.get().execute("Und Los"));
  }
}

SubServiceA = 2014-11-21T15:01:51.129
SubSubServiceImpl => 2014-11-21T14:01:51.148Z
execute = Und Los_A_SubSubServiceImpl Und Los
SubServiceB = 2014-11-21T15:01:51.167
SubSubServiceImpl => 2014-11-21T14:01:51.168Z
execute = Und Los_B_SubSubServiceImpl Und Los
SubServiceA = 2014-11-21T15:01:51.168
SubSubServiceImpl => 2014-11-21T14:01:51.168Z
execute = Und Los_A_SubSubServiceImpl Und Los

Besonderheiten von Dagger

Es gibt einige Besonderheiten bei Dagger, die, je nach Betrachtungsweise, ein Bug oder ein Feature sein können. Bis jetzt gingen wir einfach davon aus, dass es immer nur einen Objekt-Graphen geben wird – das war eine implizite Annahme. Was aber passiert, wenn wir n Instanzen des Objekt-Graphen erzeugen? Wenn wir zwei verschiedene ObjectGraph-Instanzen in der Klasse Main erzeugen lassen, bekommen wir immer eine neue Instanz. So weit, so gut. Allerdings gibt es bei Singletons auch immer eine neue Instanz. Das bedeutet, dass Singletons immer nur pro ObjectGraph-Instanz gültig sind.

@Provides @Singleton
SubSubService provideSubSubService() {
  return new SubSubServiceImpl();
}
//..
final ObjectGraph objectGraphA = ObjectGraph.create(new BusinessModule());
final ObjectGraph objectGraphB = ObjectGraph.create(new BusinessModule());

//Singletons gelten nur per ObjectGraph
final Main mainA = objectGraphA.get(Main.class);
final Main mainB = objectGraphB.get(Main.class);
mainA.mainService.execute("Und Los");
mainB.mainService.execute("Und Los");

SubServiceA = 2014-11-21T15:10:58.054
SubSubServiceImpl => 2014-11-21T14:10:58.071Z
SubServiceA = 2014-11-21T15:10:58.090
SubSubServiceImpl => 2014-11-21T14:10:58.090Z

Aber es gibt noch etwas zu beachten: Es besteht ja auch noch die Option, eine Instanz mittels der Methode inject(..) zu initialisieren. Der Container kontrolliert nicht, ob das bei einer Instanz schon einmal durchgeführt worden ist. Es gibt auch keine offizielle Prüfmethode, sondern man muss selber Attribute z. B. auf null prüfen. Ein wenig verwirrend wird es, wenn man das nun auch zwischen verschiedenen Objekt-Graphen macht. 

Main main = new Main();
final ObjectGraph objectGraphA = ObjectGraph.create(new BusinessModule());
final ObjectGraph objectGraphB = ObjectGraph.create(new BusinessModule());

objectGraphA.inject(main);
System.out.println("main.mainService = " + main.mainService);
System.out.println("####### next inject #######");
objectGraphA.inject(main);
System.out.println("main.mainService = " + main.mainService);
//ohoh..  jetzt wird es durcheinander....
System.out.println("####### next object graph #######");
objectGraphB.inject(main);
System.out.println("main.mainService = " + main.mainService);

SubServiceA = 2014-11-21T15:19:46.629
SubSubServiceImpl => 2014-11-21T14:19:46.646Z
main.mainService = org.rapidpm.demo.business.MainServiceImpl@17f052a3
####### next inject #######
SubServiceA = 2014-11-21T15:19:46.664
main.mainService = org.rapidpm.demo.business.MainServiceImpl@2e0fa5d3
####### next object graph #######
SubServiceA = 2014-11-21T15:19:46.666
SubSubServiceImpl => 2014-11-21T14:19:46.666Z
main.mainService = org.rapidpm.demo.business.MainServiceImpl@5010be6

Fazit

Insgesamt ist der Ansatz von Dagger sehr angenehm, auch wenn das Framework hat einige Besonderheiten hat, die es zu beachten gilt. Die Performance bei der Ausführung ist sehr gut, die Verfügbarkeit für Java und Android sicherlich ein wichtiges Entscheidungskriterium. Bedauerlich ist allerdings, dass es sich wieder nur um ein DI- und nicht um ein CDI-(Contexts and Dependency Injection)-Framework handelt

Geschrieben von
Sven Ruppert
Sven Ruppert
Sven Ruppert arbeitet seit 1996 mit Java und ist Developer Advocate bei Vaadin. In seiner Freizeit spricht er auf internationalen und nationalen Konferenzen, schreibt für IT-Magazine und für Tech-Portale. Twitter: @SvenRuppert
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu: