Suche
Ein Segen für Java-Entwickler?

Dependency Injection mit Boon DI

Sven Ruppert
© shutterstock.com/Hurst Photo

Mit Boon DI widmen wir uns nach Dagger einem weiteren Vertreter der Gattung Dependency-Injection-Frameworks. Das Projekt selbst stellt einige weitere Module/Packages bereit, auf die wir im Weiteren nicht eingehen werden. Hier werden wir uns lediglich die DI-Eigenschaften ansehen. 

In der Einleitung zu Boon DI kann man lesen, dass es Dagger ähnlich sein soll, aber auf den erweiterten Compile-Prozess verzichtet. Ebenfalls werden einige Qualifier angeboten, die namentlich denen aus den Frameworks Spring, Seam, Guice und „plain“ CDI (Contexts and Dependency Injection) gleichen. Eine Besonderheit vorweg: Es gibt den Qualifier @Inject einmal im Package org.boon.di; bei der Verwendung kann man allerdings auch auf javax.inject.Inject zurückgreifen. Die gemischte Verwendung ist ebenfalls möglich. 

Erzeugungsreihenfolge

Bei der Verwendung von DI-Frameworks ist es von Interesse, die Reihenfolge der Instanziierungen zu kennen. Um dies zu ermitteln, verwende ich folgende Hilfsklasse: PrintInfo. 

public class PrintInfo {
  public static void printInfo(Class aClass){
    final Method[] declaredMethods = aClass.getDeclaredMethods();
    String txt = "";
    for (final Method declaredMethod : declaredMethods) {
      txt = txt + " - " + declaredMethod.getName();
    }
    System.out.println(aClass.getSimpleName() + " -> " + txt);
  }
}

Das erste Beispiel ist nun eine einfache Instanziierung. Hier wird ein Service mit einem Attribut vom Typ SubService per @Inject versorgt. 

public class MainStep001 {
  public static void main(String[] args) {
    final Service service = new Service();
    service.printInfo();
    //CDi not working
    final Module module = DependencyInjection.classes(Service.class);
    final Service service1 = module.get(Service.class);
    service1.printInfo();
    //factory Methods are now available
    final Context context = DependencyInjection.context(module);
    final Service service2 = context.get(Service.class);
    service2.printInfo();
  }
}
public class Service {
  @Inject SubService subService;
  public void printInfo(){
    PrintInfo.printInfo(this.getClass());
    if(subService == null){
      System.out.println("subService = " + subService);
    } else{
      subService.printInfo();
    }
  }
  @PostConstruct
  public void printInfoPostconstruct(){
    System.out.println("printInfoPostconstruct() " 
        + this.getClass().getSimpleName());
  }
}
public class SubService {
  public void printInfo(){
    PrintInfo.printInfo(this.getClass());
  }
  @PostConstruct
  public void printInfoPostconstruct(){
    System.out.println("printInfoPostconstruct() " 
        + this.getClass().getSimpleName());
  }
}

In unserem Fall wird zuerst die letzte Instanziierung in der Kette durchgeführt. Es wird zunächst der SubService instanziiert, dann erst der Service. Boon DI unterstützt den Life-Cycle-Schritt @Postconstruct. Allerdings wird @PreDestroy nicht unterstützt. Bei diesem Beispiel kann man auch sehen, dass einer der Entwickler den kompletten Initialisierungsprozess selbst durchführen muss. Sicherlich erfolgen einige Dinge indirekt, aber der notwendige Abhängigkeitsgraph muss imperativ zusammengebaut werden. Erst, wenn die notwendigen Factory-Methoden bereitgestellt sind, kann mittels der Methode get(..) aus dem Context eine Instanz bezogen werden. Um nun das gerade gezeigte Listing vollständig lauffähig zu bekommen, muss ein Modul (ServiceModule) erzeugt werden.

Zu beachten ist Folgendes: Selbst wenn man eine Factory-Methode für die Erzeugung der Instanz der Klasse Service angibt, wird sie nicht verwendet. Es müssen demnach nur Factory-Methoden für die Klassen erzeugt werden, die irgendwo injiziert werden. 

public class MainStep002 {
  public static void main(String[] args) {
    final Service service = new Service();
    service.printInfo();
    //CDi not working
    final Module module = DependencyInjection.classes(Service.class);
    final Service service1 = module.get(Service.class);
    service1.printInfo();

    final Module module1 = DependencyInjection.module(new ServiceModule());
    final Context context = DependencyInjection.context(module, module1);
    final Service service2 = context.get(Service.class);
    service2.printInfo();
  }

  public static class ServiceModule{
//    public Service provideService(){
//      System.out.println("provideService ...");
//      return new Service();
//    }
    public SubService provideSubService(){
      System.out.println("provideSubService ...");
      return new SubService();
    }
  }
}

Die Definition eines Suppliers für die Klasse Service wurde hier über den Aufruf der Methode DependencyInjection.classes(..) durchgeführt. Wenn man sich die Implementierung der Methode classes(..) ansieht, so erkennt man, dass hier eine Instanz der Klasse SupplierModule erzeugt und verwendet wird. Bei der Navigation in die jeweiligen Implementierungen kommen wir zu der Stelle, an der die möglichen Instanziierungen extrahiert werden – in diesem Fall der Default-Konstruktor return Reflection.newInstance( type ); 

//DependencyInjection
public static Module classes( Class... classes ) {
  List wrap = Lists.wrap(ProviderInfo.class, classes);
  return new SupplierModule(wrap);
}
//SupplierModule
public SupplierModule( List suppliers ) {
  supplierExtraction( suppliers.toArray(new ProviderInfo[suppliers.size()]) );
}

// inside supplierExtraction
    if ( supplier == null ) {
      supplier = createSupplier( providerInfo.prototype(), type, 
                                      providerInfo.value() );
      providerInfo = new ProviderInfo(named, type, supplier, 
                                        providerInfo.value());
    }
//createSupplier
private Supplier createSupplier( final boolean prototype, 
                                 final Class type, final Object value ) {
  if ( value != null && !prototype) {
    return new Supplier() {
      @Override
      public Object get() {return value; }
    };
  } else if (value!=null && prototype) {
    return new Supplier() {
      @Override
      public Object get() {return BeanUtils.copy(value); }
    };
  } else if ( type != null ) {
    return new Supplier() {
      @Override
      public Object get() {return Reflection.newInstance( type ); }
    };
  } else {
    return new Supplier() {
      @Override
      public Object get() {  return null; }
    };
  }
}

Der Umgang mit Interfaces

Bis jetzt haben wir die Klasse Service direkt verwendet. Nun werden wir die Implementierung in die Klasse ServiceImpl auslagern, und Service wird zu einem Interface. Wie erfolgt nun die Instanziierung? Der Default-Konstruktor kann hier nicht verwendet werden. Also wird der Weg über eine Factory-Methode gehen. Das Matching findet wieder über den Rückgabetyp (Interface Service) und den Methodennamen, der mit provide beginnt, statt. Das führt zu einer Unschärfe, die einem bewusst sein sollte. Beginnen wir mit dem folgenden Listing. 

public interface Service {
  public void printInfo();
}
public class ServiceImpl implements Service {
  @Override
  public void printInfo() {
    PrintInfo.printInfo(this.getClass());
  }
}
public static class ServiceModuleA{
  public Service provideServiceA(){
    System.out.println("provideServiceAA ...");
    return new ServiceImpl();
  }
}
public static void main(String[] args) {
  final Module moduleA = DependencyInjection.module(new ServiceModuleA());
  final Context context = DependencyInjection.context(moduleA);
  final Service service2 = context.get(Service.class);
  service2.printInfo();
}
//Ausgabe
provideServiceAA ...
ServiceImpl ->  - printInfo

Die Ausgabe hier ist wie erwartet. Was aber passiert, wenn wir Mehrdeutigkeiten haben? Probieren wir es aus und erweitern die Klasse ServiceModule um eine weitere Methode zum Erzeugen der Instanz Service

public static class ServiceModuleA{
  public Service provideServiceA(){
    System.out.println("provideServiceAA ...");
    return new ServiceImpl();
  }
  public Service provideServiceB(){
    System.out.println("provideServiceAB ...");
    return new ServiceImpl();
  }
}
//Ausgabe
provideServiceAB ...
ServiceImpl ->  - printInfo

Wir sehen, dass nun die zweite Methode für die Erzeugung verwendet wird. Fehler-/Info-Meldungen sucht man übrigens vergeblich. Und wenn wir die Klasse ServiceModuleA nun um eine weitere Methode erweitern?  

public static class ServiceModuleA{
  public Service provideServiceA(){
    System.out.println("provideServiceAA ...");
    return new ServiceImpl();
  }
  public Service provideServiceB(){
    System.out.println("provideServiceAB ...");
    return new ServiceImpl();
  }
  public Service provideServiceC(){
    System.out.println("provideServiceAC ...");
    return new ServiceImpl();
  }
}
//Ausgabe
provideServiceAB ...
ServiceImpl ->  - printInfo

Es wird immer noch die zweite Implementierung verwendet. Aber was ist, wenn man nun noch mehr als ein Modul definiert – auch hier wieder erst mit einer Methode und dann Schritt für Schritt um zusätzliche Methoden erweitert? Hier zeigt sich, dass, je nachdem, in welcher Reihenfolge die Module im Context registriert werden, immer jeweils die B-Version genommen wird. Das Verhalten ist reproduzierbar, jedoch nicht intuitiv. Demnach muss man bei dem Aufbau der Anwendung sehr genau darauf achten, was in welcher Reihenfolge registriert wird. Bei kleinen Implementierungen kann das sicherlich schnell verifiziert werden. Bei komplexeren Konstrukten sollte man hier gezielt prüfen.

Aber warum wird immer die B-Implementierung genommen? Die Lösung ist recht einfach: Wenn man von einer Klasse die deklarierten Methoden mittels der Methode getDeclaredMethods() erhält, ist dort keine lexikongraphische Sortierung in dem ErgebnisArray vorhanden. Da aber überall mittels Reflection die Informationen zusammengetragen werden, kommt es hier zu diesem Verhalten. Als Beispiel dient das nachfolgende Listing. 

public class ReflectionDemo {
  public static void main(String[] args) {
    final Method[] declaredMethods = DemoKlasse.class.getDeclaredMethods();
    for (final Method declaredMethod : declaredMethods) {
      System.out.println("declaredMethod = " + declaredMethod);
    }
  }

  private static class DemoKlasse{
    public void methodA(){}
    public void methodB(){}
    public void methodC(){}
  }
}

Aufmacherbild: „needle and injection in bottle“ von shutterstock.com / Urheberrecht: Hurst Photo

[ header = Prototypen und weitere Besonderheiten ]

Prototypen

Boon DI bietet die Möglichkeit, das Prototypen-Pattern zu verwenden. Hierzu werden Instanzen, die als Vorlage dienen sollen, dem Context übergeben, natürlich verpackt in eine Instanz eines Moduls. Am einfachsten ist das mit einfachen POJOs realisiert. 

public static class DemoKlasse {
    private String name;
    private int nummer;

    public String getName() {
      System.out.println("getName = " + name);
      return name;
    }

    public void setName(String name) {
      this.name = name;
    }

    public int getNummer() {
      System.out.println("getNummer = " + nummer);
      return nummer;
    }

    public void setNummer(int nummer) {
      this.nummer = nummer;
    }
  }
    final DemoKlasse demoKlasse = new DemoKlasse();
    final Module module = DependencyInjection.prototypes(demoKlasse);
    final Context context = DependencyInjection.context(module);
    final DemoKlasse d1 = context.get(DemoKlasse.class);
    System.out.println("proto = " + demoKlasse);
    System.out.println("   d1 = " + d1);

Die Verwendung zeigt uns, dass hier zwei verschiedene Instanzen in Summe erzeugt werden. Die Getter werden nicht aufgerufen. Wie nun sieht es mit komplexen Attributen aus? Erzeugen wir uns nun eine Klasse DemoKlasse mit zwei normalen (String- und int-) Attributen und einem Attribut der Klasse SubDemoKlasse. Die Klasse SubDemoKlasse implementiert das Interface SubDemo. Wenn wir nun eine Instanz als Prototyp übergeben, erhalten wir aus dem Context immer eine Deep Copy zurück. Allerdings sollte man derzeit die Verwendung von neuen Java-8-Klassen wir z. B. LocalDateTime vermeiden. Hier gibt es noch Exceptions, da nicht korrekt mittels Reflection in der Vererbung dieser Klassen gearbeitet wird.

public static void main(String[] args) {
    final DemoKlasse demoKlasse = new DemoKlasse();
    demoKlasse.setName("DemoKlasse");
    demoKlasse.setNummer(1);
    final SubDemoKlasse subDemoKlasse = new SubDemoKlasse();
    subDemoKlasse.setName("SubDemoKlasse");
    subDemoKlasse.setNummer(2);
//    subDemoKlasse.setLocalDateTime(LocalDateTime.now());
    demoKlasse.setSubDemoKlasse(subDemoKlasse);
    final Module module = DependencyInjection.prototypes(demoKlasse);
    final Context context = DependencyInjection.context(module);
    final DemoKlasse d1 = context.get(DemoKlasse.class);
    System.out.println("proto = " + demoKlasse);
    System.out.println("   d1 = " + d1);
  }
  public static class DemoKlasse {
    private String name;
    private int nummer;
    private SubDemo subDemoKlasse;
    //snipp getter/setter
  }
  public static interface SubDemo {   }
  public static class SubDemoKlasse implements SubDemo {
    private String name;
    private int nummer;
//    private LocalDateTime localDateTime;
    //snipp getter/setter
  }

Kann man hier nun Prototypen verwenden, die selber wiederum ein @Inject verwenden? Ja, kann man, wie nachfolgendes Listing zeigt. 

public static void main(String[] args) {
  final Module module = DependencyInjection.module(new DemoModule());
  final Context ctx = DependencyInjection.context(module);
  final DemoKlasse demoKlasse = ctx.get(DemoKlasse.class);
  demoKlasse.setName("DemoKlasse");
  final SubDemoKlasse subDemoKlasse 
      = (SubDemoKlasse) demoKlasse.getSubDemoKlasse();
  subDemoKlasse.setName("SubDemoKlasse");
  final Module moduleProto = DependencyInjection.prototypes(demoKlasse);
  final Context context = DependencyInjection.context(moduleProto,module);
  final DemoKlasse d1 = context.get(DemoKlasse.class);
  System.out.println("proto = " + demoKlasse);
  System.out.println("proto.getSubDemoKlasse() = " 
      + demoKlasse.getSubDemoKlasse());
    System.out.println("   d1 = " + d1);
    System.out.println("d1.getSubDemoKlasse()    = " 
      + d1.getSubDemoKlasse());
  }
  public static class DemoModule{
    public DemoKlasse provideDemoKlasse(){ return new DemoKlasse();}
    public SubDemo provideSubDemoKlasse(){ return new SubDemoKlasse();}
  }
  public static class DemoKlasse {
    private String name;
    @Inject SubDemo subDemoKlasse;
    //snipp
  }
  public static interface SubDemo { }
  public static class SubDemoKlasse implements SubDemo {
    private String name;
    //snipp
  }

Ebenfalls bietet Boon DI die Möglichkeit, Singletons zu registrieren. Das wird ebenfalls in der Form realisiert, dass die Instanz manuell erzeugt werden muss und dann als Singleton dem Context übergeben wird:

Module m = ContextFactory.objects( singleton );

 

Weitere Besonderheiten von Boon DI

Boon DI bietet auch die Unterstützung für den Qualifier @Named(). Hiermit lassen sich verschiedene Implementierungen bzw. Initialisierungen abbilden und dann an der Stelle, an der ein @Inject erfolgt, mit dem zusätzlichen @Named(..) konkretisiert werden. Beginnen wir mit einer Klasse ServiceImpl und deren zwei Spezialisierungen ServiceImplA und ServiceImplB.   

//no interfaces
  public static class ServiceImpl{
    public String work(String txt){ return "ServiceImpl - " + txt; }
  }
  public static class ServiceImplA extends ServiceImpl{
    public String work(String txt){ return "ServiceImplA - " + txt; }
  }
  public static class ServiceImplB extends ServiceImpl{
    public String work(String txt){ return "ServiceImplB - " + txt; }
  }

Um diese Implementierungen nun mittels @Named() verwenden zu können, muss man die jeweiligen Provider per Modul registrieren:

final Module suppliers = DependencyInjection.suppliers(
  ProviderInfo.providerOf(ServiceImpl.class, ServiceImplA::new),//default
  ProviderInfo.providerOf("ImplA", ServiceImpl.class, ServiceImplA::new),
  ProviderInfo.providerOf("ImplB", ServiceImpl.class, ServiceImplB::new)
    );
System.out.println(context.get(ServiceImpl.class).getClass().getSimpleName());
System.out.println(context.get(ServiceImpl.class, 
  "ImplA").getClass().getSimpleName());
System.out.println(context.get(ServiceImpl.class, 
  "ImplB").getClass().getSimpleName());

Aber wie sieht es nun bei der Verwendung von Interfaces aus? Hier kann man diesen Weg leider nicht gehen. Auch hier liegt es wieder an der Verwendung von Reflection bzw. an der Art, wie sie realisiert worden ist. An einer Stelle wird erwartet, dass der Zieltyp einen Supertyp hat. Leider ist das bei Interfaces nicht der Fall und führt bei der Implementierung von Boon DI zu einer NullPointerException. Um auf Interfaces zurückzuführen, fehlt also die Implementierung der Factory-Methoden. Das kann natürlich auf dem bis jetzt bekannten Wege passieren. Innere anonyme Klassen können einem allerdings viel Schreibarbeit ersparen.

//interfaces
public static interface Service{ public String work(String txt); }
public static class ServiceA implements Service{
  public String work(String txt){return "ServiceA - " + txt;}
}
public static class ServiceB implements Service{
  public String work(String txt){ return "ServiceB - " + txt; }
}
//with interfaces
final Module module = DependencyInjection.module(new Object() {
  public Service provide(){  return new ServiceA();}
  @Named("A") public Service provideA(){ return new ServiceA();}
  @Named("B") public Service provideB(){ return new ServiceB();}
});

final Context ctx = DependencyInjection.context(module);
System.out.println(ctx.get(Service.class).getClass().getSimpleName());
System.out.println(ctx.get(Service.class, "A").getClass().getSimpleName());
System.out.println(ctx.get(Service.class, "B").getClass().getSimpleName());

Die Klasse Context bietet noch die Methode invoke(..). Hiermit kann man eine beliebige Methode einer angeforderten Instanz aufrufen. Dabei handelt es sich lediglich um eine Kombination aus dem Holen einer Instanz und nachträglichem Aufruf der Methode mittels Reflection. Hierbei ist es natürlich egal, ob die Methode als public oder private deklariert ist. 

final Module suppliers = DependencyInjection.suppliers(
  ProviderInfo.providerOf("", ServiceImpl.class, ServiceImplA::new),//default
  ProviderInfo.providerOf("ImplA", ServiceImpl.class, ServiceImplA::new),
  ProviderInfo.providerOf("ImplB", ServiceImpl.class, ServiceImplB::new)
  );

final Context context = DependencyInjection.context(suppliers);

//kein Zugriff ohne Named
System.out.println(context.invoke("", "work", "invoked.."));
System.out.println(context.invoke("ImplA", "work", "invoked.."));
System.out.println(context.invoke("ImplB", "work", "invoked.."));

Fazit

Boon DI birgt einige interessante Details. Der Ansatz, alles mittels Reflection zur Laufzeit zu realisieren, hat sowohl Vor- als auch Nachteile. Welche davon überwiegen, ist natürlich in Abhängigkeit von dem Projekt zu sehen. Allerdings ist Boon DI ein guter Startpunkt, wenn man auf dieser Basis eine CDI für sein Projekt realisieren möchte, die nur spezielle Funktionen unterstützen muss und z. B. Weld nicht die passende Lösung darstellt. Die Eigenheit der Sichtbarkeit verschiedener Factory-Methoden muss in größeren Projekten allerdings genau bedacht werden.

Geschrieben von
Sven Ruppert
Sven Ruppert
Sven Ruppert entwickelt seit 1996 Software. Er ist Fellow (Leiter der Forschung & Entwicklung) bei Reply in München. In seiner Freizeit spricht er auf internationalen und nationalen Konferenzen, schreibt für IT-Magazine und für Tech-Portale. Twitter: @SvenRuppert
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.