Suche
Kontext ist alles

Warum das C in CDI den Unterschied macht

Sven Ruppert
Kontext ist alles

© Shutterstock / JazzIRT

Immer mehr Dependency-Injection-(DI-)Frameworks erblicken das Licht der Welt, beispielsweise „Dagger“ und „Boon“. Darunter sind allerdings nur wenige CDI-Frameworks, was bedauerlich ist – schließlich ist das C (für „Contexts“) doch das eigentlich Interessante an CDI. Der folgende Beitrag zeigt, warum das C den Unterschied macht und wie man es einsetzen kann – übrigens nicht nur auf der Java-EE-Seite.

Im Folgenden werden wir uns verschiedene Einsatzmöglichkeiten von CDI ansehen, an denen gezeigt wird, wofür das „C“ steht und weshalb es so nützlich ist. Dabei werde ich ausschließlich das Core-JDK und die CDI-Spezifikation verwenden. Als Basisimplementierung kommt der WELD-Container zum Einsatz.

Doch zunächst zu den Zielen von CDI: Hier gibt es sicher verschiedene Interpretationen. Wir gehen im Folgenden davon aus, dass wir eine Methode haben und nennen diese execute(). Als Rückgabewert kommt eine Liste zum Einsatz:

public List execute(){ ... };

Und hier fängt es schon an: Es handelt sich um ein Interface. Wir wissen also nicht, ob sich dahinter eine ArrayList oder eine LinkedList verbirgt. Es kann natürlich eine beliebige Implementierung sein. Und damit machen wir auch alles richtig, solange wir keine speziellen Methoden einer Implementierung auf diesem Rückgabewert anwenden wollen. Als Beispiel sei hier die Methode trimToSize() von der Implementierung ArrayList genannt. Aber ist damit eine Entkopplung erreicht? Ich sage einfach mal: nein. Doch wo beginnt eine Kopplung und wo endet sie? Sehen wir uns dazu erst einmal das Innenleben der Methode execute() an:

import java.util.List;
import java.util.ArrayList;

public List<Data>; execute(){
  final List<Data>; result = new ArrayList<>;();
  //....
  return result;
}

Im Inneren haben wir uns für die Implementierung der ArrayList entschieden. Damit ist an dieser Stelle alles fest verdrahtet. Aber ergibt diese Entscheidung Sinn? Wir wissen es nur, wenn wir genug über die Eigenschaften der Anwendung wissen. Eigentlich wäre es nützlicher, zur Laufzeit zu entscheiden, welche Implementierung der Umgebung am besten entspricht. Aber nicht nur da haben wir uns in der statischen Semantik festgesetzt. Der Import ist natürlich genauso eine Kopplung. Aber dazu gleich mehr.

Wollen wir nun die Erzeugung der ArrayList auslagern, kann man zum Beispiel zu dem Pattern Factory greifen. Und damit man die Auswahl hat, welche Implementierung es werden soll, bietet uns die Factory gleich mehrere Methoden an. Selbstverständlich kann es auch durch Parameter gesteuert werden. Das macht aber für die Überlegung keinen Unterschied.

Listing 1 

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

public class ListFactory {
  public List createArrayList()  { return new ArrayList(); }
  public List createLinkedList() { return new LinkedList(); }
  public List createList()       { return new ArrayList(); }
}

Listing 1 zeigt eine beispielhafte Implementierung einer Factory, die einen auswählen lässt, was man erzeugen möchte. Das führt zu einer sehr hohen Kopplung an einer Stelle. Da hier alle für die Auswahl zur Verfügung stehenden Implementierungen zusammenlaufen, ist dies ein Knotenpunkt, den man eigentlich verhindern wollte. Aber haben wir an der Stelle, an der die Factory verwendet wird, eigentlich etwas erreicht? Ich bin der Meinung, dass das nicht der Fall ist:

import java.util.List;
import org.rapidpm.demo.ListFactory;
public List<Data>; execute(){
  final List list = new ListFactory().createArrayList();
  //....
  return result;
}

Denn bei der Verwendung haben wir zum einen die Abhängigkeit der ArrayList im Import durch einen Import der Factory ersetzt. Die Abhängigkeit zu der Factory stellt eigentlich sogar eine schlechtere Kopplung dar. Und zum anderen haben wir durch den expliziten Aufruf der Methode zum Erzeugen der ArrayList genau die gleiche Verbundenheit wie vorher. Einziger Unterschied ist nun, dass man Initialisierungen an einer zentralen Stelle durchführen kann – allerdings nur, wenn das notwendig ist. Und zur Laufzeit können wir noch immer nichts austauschen. Also auch dieses Ziel ist noch nicht erreicht worden.

Es gibt noch eine Kleinigkeit, die es zu beachten gilt: Alle Entwickler eines Projekts müssen von dieser Factory Kenntnis haben, um diese auch zu verwenden. Die Verwendung wird also mit einiger Wahrscheinlichkeit unterschiedlich ausfallen.

Eine dynamischere Lösung

Wie sieht nun eine etwas dynamischere Lösung aus? Ausschlaggebend ist der Zeitpunkt der Entscheidung, den wir uns im Folgenden ansehen. Dazu müssen wir den Methodenaufruf so gestalten, dass die Signatur implementierungsneutral ist. Definieren wir also an der Stelle einen ContextResolver. Dieser soll in der Lage sein, über den derzeitigen Zustand der Systemumgebung in adäquater Weise Auskunft zu geben, damit man in der Lage ist, die notwendigen Entscheidungen zu treffen. In unserem Fall müssen wir uns für eine Implementierung des Interface List entscheiden können.

Listing 2

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import org.rapidpm.demo.cdi.commons.registry.ContextResolver;

public class ListFactory {
  public List createList(final ContextResolver contextResolver){
    if(contextResolver == null){ return createArrayList(); }
    else {
      if(contextResolver.resolveContext()){ return createArrayList(); }
      else{ return createLinkedList(); }
    }
  }
}

Im Beispiel in Listing 2 habe ich mich auf eine Auswahl von genau zwei Elementen beschränkt. Aber das Prinzip sollte deutlich werden. Es stellt sich nun die Frage, wie man an den ContextResolver kommt. Außerdem hat man hiermit eigentlich nur erreicht, dass die Komplexität im ContextResolver liegt.

Gleichwohl sind wir nun in der Lage, zur Laufzeit der Situation entsprechende Entscheidungen zu treffen. Nun muss dafür gesorgt werden, dass die Implementierung der Factory nicht statisch alle Implementierungen kennen muss. Hier kann man nun mit einer Registry usw. beginnen. Was auf jeden Fall klar geworden ist, ist die Tatsache, dass man jede Menge Infrastrukturcode schreibt. Und hier kommt nun CDI – und nicht nur DI – zum Einsatz.

Setzen wir an der Stelle an, an der wir eine Instanz der Liste benötigen. Hier beginnen wir mit einem @Inject. Da es aber mehr als eine Implementierung der Liste geben wird, müssen wir hier schon ein wenig konkreter werden. Also wird aus einem @Inject ein @Inject @MyQualifier. Dazu gibt es dann einen passenden Producer, in dem wir die Logik, die wir vorher in der Factory hatten, unterbringen können (Listing 3).

Listing 3

@Inject @CDILegacyTest List list;

@Produces @CDILegacyTest
public List createList(InjectionPoint injectionPoint,
                       BeanManager beanManager, 
                       ContextResolver contextResolver){

  // Treffen der Entscheidungen...
  boolean b = contextResolver.resolveContext(...);  
  if(b){ return new LinkedList(); } else { return new ArrayList(); }
}

Und damit der Zeitpunkt der Entscheidung möglichst nahe an dem Zeitpunkt der Verwendung liegt, ersetzen wir noch das Interface List durch den Virtual Proxy Instance<T> (Listing 4).

Listing 4

@Inject @CDILegacyTest Instance<List> listInstance;
// ..später
final List list = listInstance.get();

@Produces @CDILegacyTest
public List createList(InjectionPoint injectionPoint,
                       BeanManager beanManager, 
                       ContextResolver contextResolver){
        
  // Treffen der Entscheidungen...
  boolean b = contextResolver.resolveContext(...);  
  if(b){ return new LinkedList(); } else { return new ArrayList(); }
}

Qualifier und AnnotationLiteral

Bei der Verwendung von Qualifiern hat man immer eine 1-1-Beziehung zwischen dem InjectionPoint und dem Producer:

@Inject @CDILogger Logger logger;
@Producer @CDILogger Logger create(...);

Was kann man nun mit AnnotationLiterals erreichen? Sehen wir uns das Innere der Methode create() an:

@Producer @CDILogger Logger create(BeanManager bm,ContextResolver cr){
  AnnotationLiteral prodAL = contextResolver.resolve(..);
  return creator.getManagedInstance(Logger.class, AnnotationLiteral);
};

Wenn man also vom ContextResolver eine Instanz eines AnnotationLiteral<T> zurückbekommen kann, kann man damit einen Zielproducer aussuchen. Das ermöglicht nun einiges, da man nur noch die Zielimplementierung im Klassenpfad benötigt. Keine Definitionen in XML o. Ä. sind notwendig. Die ContextResolver selbst sollten natürlich auch so implementiert sein, dass es nicht notwendig ist, alle manuell aufzurufen. Also gehen wir einfach davon aus, dass ein ContextResolver für einen Teilbereich der Applikation zuständig ist. Dann muss er selbst entscheiden können, ob er eine Antwort liefern muss oder nicht. In diesem Beispiel habe ich einfach anhand der übergebenen Klasse den Package-Namen verwendet. In anderen Systemen wird es sicherlich anders aussehen. Wichtig an dieser Stelle ist es allerdings, dass es keine Überschneidungen zwischen den einzelnen Bereichen der ContextResolver gibt.

Aber auch das kann man lösen. Damit man nicht wieder alle ContextResolver deklarieren muss, weder in XML noch in Java-Code, kann man folgenden Weg gehen: Es gibt im System ein Interface ContextResolver. Von diesem erbt der jeweilige ContextResolver. Über den BeanManager kann man nun alle Implementierungen von diesem Interface erhalten. Die gehen wir linear durch und fragen, ob er für die gerade angefragte Klasse zuständig ist. Falls ja, liefert er ein Ergebnis für den derzeitigen Zustand. Dieses Vorgehen kann man ruhig für bis zu einigen hundert ContextResolvern wählen. Sind mehr vorhanden, kann man über andere Strukturen nachdenken. Für das Verständnis sollte das aber auf jeden Fall ausreichen. Nun sind wir also in der Lage, einen generischen ContextResolver in jeden Producer zu injizieren (Listing 5).

Listing 5

public Set<ContextResolver> gettAllContextResolver() {
  final Set<ContextResolver> resultSet = new HashSet<>();
  final Set<Bean<?>> allBeans 
    = beanManager.getBeans(ContextResolver.class, 
                           new AnnotationLiteral<Any>() {});
  allBeans.forEach(b-> b.getTypes().stream()
    .filter(t -> t.equals(ContextResolver.class))
    .forEach(t -> {
        final ContextResolver cr = ((Bean<ContextResolver>) b)
        .create(beanManager.createCreationalContext(
            (Bean<ContextResolver>) b));
        resultSet.add(cr);
  }));
  return resultSet;
}

@Override
public AnnotationLiteral resolveContext(Class<?> targetClass) {
  final Stream<ContextResolver> contextResolversMocked = 
    gettAllMockedContextResolver().stream();
  final Stream<ContextResolver> contextResolvers = 
    gettAllContextResolver().stream();

  return contextResolversMocked
  .filter(r -> (r.resolveContext(targetClass) != null))
  .map(r -> r.resolveContext(targetClass))
  .findFirst()
  .orElse(
    contextResolvers
    .filter(r -> !r.getClass()
      .isAnnotationPresent(CDICommonsMocked.class))
    .filter(r -> !r.getClass().equals(DefaultContextResolver.class))
    .filter(r -> (r.resolveContext(targetClass) != null))
    .map(r -> r.resolveContext(targetClass))
    .findFirst()
    .orElse(null)
  );
}

Sehen wir uns das Ganze aus der Sicht eines Entwicklers an. Gehen wir davon aus, dass ein Kontext ein Test mit JUnit ist. Hier möchte man zum Beispiel einen Mock per @Inject erhalten (Listing 6).

@Inject CDIContext context; // z. B. ein Singleton
@Inject ContextResolver cr;

@Test
public void testMockedModus001() throws Exception {
  Assert.assertFalse(context.isMockedModusActive());
  final AnnotationLiteral annotationLiteralProd =    
                                   cr.resolveContext(this.getClass());
  Assert.assertEquals(new AnnotationLiteral<CDICommons>() {},
    annotationLiteralProd);
   
  // Kontext aendert sich, hier manuell
  ((TestContext)context).setTestModus(true);
  Assert.assertTrue(context.isMockedModusActive());
  final AnnotationLiteral annotationLiteral = 
    cr.resolveContext(this.getClass());
  Assert.assertEquals(new AnnotationLiteral<CDICommonsMocked>() {},
    annotationLiteral);
}

An den Stellen, an denen ein @Inject erfolgt, wird im jeweiligen ersten Producer entschieden, dass es sich nun um einen Mock handeln soll:

@Inject @MyQ 
   -> MyQ-Producer 
      -> AnnotationsLiteral<MyOtherQ> (Beanmanager -> Interface, MyOtherQ)
          -> MyOtherQ-Producer

Der Aufruf beim BeanManager erfolgt also mit der Angabe des Interface und dem aufgelösten AnnotationLiteral. Damit ist der statische Qualifier vollständig entkoppelt von dem tatsächlich verwendeten Producer. Und das alles mit den Bordmitteln von CDI.

C für alle

Wir haben die Möglichkeiten von CDI mit Blick auf das „C“ – für „Contexts“ – hier nur knapp beleuchtet. Wenn man ein wenig damit experimentiert hat, so ergeben sich sehr flexible Patterns, mit denen man sehr dynamische Systeme bauen kann. Spielen Sie ein wenig damit herum, es lohnt sich!

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: