"
 
 1 Einführung in CDI
"
 
 2 CDI Grundkonzepte
"
 
 3 CDI und Java EE
"
 
 4 Portable CDI-Erweiterungen
"
 
 5 Apache DeltaSpike
"
 
 6 CDI Lite
"
 
 7 CDI oder nicht CDI
"
 
 8 CDI in der Cloud

7 CDI oder nicht CDI

Die Relevanz einer beliebten Technologie stellt sich oft erst nach Jahren heraus, sobald der anfängliche Hype abgeklungen ist. Bei CDI ist das Ergebnis eindeutig. Viele Java EE Applikationen profitieren von CDI, da Applikations- und Testlogik sehr effizient umgesetzt werden können. Darüber hinaus wird aktuell mit jeder Version von Java EE die CDI-Integration anderer Spezifikationen wie bspw. JSF weiter ausgebaut, wodurch CDI immer stärker zu einem essenziellen Aspekt von Java EE wird. Im Falle von JSF werden verschiedene neue Funktionalitäten sogar nur noch in Verbindung mit CDI unterstützt.

 

Auch außerhalb der Java EE Plattform erfreut sich CDI großer Beliebtheit. So gut wie jedes populäre Framework, welches mit Dependency-Injection Frameworks integriert ist, unterstützt CDI direkt oder ermöglicht zumindest eine einfache Integration. Es ist mittlerweile gar nicht mehr einfach ein beliebtes Framework ohne CDI-Modul zu finden. Ein Applikationsframework ohne mitgeliefertem CDI-Support ist das Aktorenframework Akka (siehe http://akka.io). Dies nehmen wir zum Anlass, um die einfache Integration von CDI mit anderen Frameworks zu veranschaulichen.

7.1 CDI-Integration durch Producer

Bevor wir mit der Integration von CDI und Akka beginnen können, ist es wichtig die grundlegenden Eigenschaften beider Technologien zu kennen und zu verstehen.

 

Zu CDI haben wir uns bereits in den vorherigen Kapiteln einige Details angesehen. Daher können wir uns an dieser Stelle auf die wichtigsten Aspekte von Akka beschränken. Das zentrale Konzept von Akka sind Aktoren. Aktoren verarbeiten Nachrichten asynchron, wobei eine Aktoreninstanz für die Verarbeitung einer Nachricht von einem Thread ausgeführt wird. Sendet ein Aktor eine Nachricht an einen anderen Aktor, dann erfolgt die Weiterverarbeitung im Normalfall durch einen anderen Thread. Daher können wir nicht wie bei Java EE davon ausgehen, dass es eine Art Request gibt der von einem Thread komplett verarbeitet wird und nur im Ausnahmefall asynchron verzweigt. Lediglich die Nachrichtenverarbeitung innerhalb eines Aktors ist einem bestimmten Thread zugeordnet, wodurch Threadsicherheit garantiert ist. Einem Aktor kann über eine sogenannte Aktorenreferenz eine Nachricht übermittelt werden. Diese Referenz ist nicht wie eine Contextual-Reference von CDI unbeschränkt gültig. Da Akka in einigen Versionen andere Regeln für verschiedene Aktorentypen definiert, ist für die nachfolgenden Integrationsschritte primär wichtig, dass Aktorenreferenzen nicht immer unbeschränkt gültig sind und es sicherer ist regelmäßig neue Referenzen zu erzeugen.
Tipp: Eine weitere Regel, die spezifisch für Akka gilt, betrifft den Scope einer Aktoreninstanz. In der Dokumentation von Akka wird empfohlen nur Instanzen zu verwenden, die durch keinen anderen Container verwaltet werden. Im Falle von CDI bedeutet dies bspw. die Verwendung von einfachen dependent-scoped Beans für Aktoren. Denn für dependent-scoped Beans ohne Interceptoren und Decoratoren erzeugt der CDI-Container normalerweise keine Contextual-Referenz in Form eines Proxies. In den Aktor-Instanzen selbst können wir CDI-Beans mit anderen Scopes injizieren, sofern der entsprechende Context beim Zugriff auf die Contextual-Reference aktiv ist. Die Injizierung muss bei Akka jedoch via Constructor-Injection erfolgen.

 

In IdeaForkLite können wir Akka zum asynchornen Versenden von CDI-Events verwenden. Da Aktoren selbst bereits in eigenen Threads ausgeführt werden, müssen wir hierfür nur CDI-basierte Injizierung für Aktoreninstanzen ermöglichen, um in diesen anschließend CDI-Events synchron zu feuern. Darüber hinaus können wir auch für CDI-Beans das Versenden von Nachrichten an Aktoren erleichtern. In EntityProcessorInterceptor von IdeaForkLite wird bisher das Event IdeaChangedEvent synchron gefeuert. Um über einen Aktor die asynchrone Variante dieses Events zu feuern, müssen wir eine entsprechende Aktorenreferenz hinzufügen. Da Interceptoren die Lebensdauer des jeweils intercepteten CDI-Beans erhalten, sollten wir diese Aktorenreferenz nicht direkt injizieren. Technisch wäre dies zwar möglich, aber wie eingangs erwähnt können Aktorenreferenzen ungültig werden. Somit bietet es sich an einen möglichst kurzen Scope für CDI-Beans zu wählen, in welchen Aktorenreferenzen injiziert werden sollen. Eine mögliche Variante in IdeaForkLite ist die Auslagerung der zuvor erwähnten Event-Logik in ein CDI-Bean namens IdeaEventBroadcaster , welches bspw. im bereits vorgestellten Transaction-Scope von DeltaSpike abgelegt wird. Wie wir in den weiteren Schritten sehen werden, muss die Aktorenreferenz selbst ein dependent-scoped CDI-Bean sein. Durch die Kombination aus einer dependent-scoped Aktorenreferenz und der Verwendung in einem Transaction-Scope Bean beschränken wir die Lebensdauer einer Aktorenreferenz in unserem Beispiel auf eine Transaktion.

 

Akka stellt für Aktorenreferenzen eine eigene Klasse namens ActorRef zur Verfügung. Da diese Klasse unabhängig von einem konkreten Aktor ist, müssen wir, wie in Listing Notifizierung von Akka-Aktoren zu sehen ist, einen eigenen CDI-Qualifier namens @Actor verwenden. Dieser selbst definierte CDI-Qualifier ermöglicht die Angabe der Aktorenklasse, damit wir später feststellen können welchem Aktor die Nachricht übermittelt werden soll. Den dazugehörigen Producer werden wir in einem der nächsten Schritte betrachten.
@TransactionScoped
public class EntityChangedEventBroadcaster {
  //...

  @Inject
  @Actor(type = IdeaChangedEventActor.class)
  private ActorRef asyncBroadcaster;

  public void broadcastIdeaChangedEvent(Idea entity) {
    IdeaChangedEvent ideaChangedEvent = new IdeaChangedEvent(entity);
    this.asyncBroadcaster.tell(ideaChangedEvent, this.asyncBroadcaster);
    //...
  }
}
Mit der Methode ActorRef#tell können wir dem dahinterliegenden Aktor eine Nachricht senden, welche asynchron durch Akka zugestellt wird. Die Verarbeitung der so übermittelten Nachricht kann bspw. wie in Listing Aktor mit CDI-Support erfolgen. Die einzige Vorgabe von Akka zur Implementierung eines Java-Aktors ist hierbei die Erweiterung der Klasse UntypedActor , wodurch die Aktorenmethode #onReceive vorgegeben wird. Jede Nachricht an einen Aktor wird durch diese Methode in einem eigenen Aktor-Thread verarbeitet.
In Listing Aktor mit CDI-Support wird zusätzlich über Constructor-Injection ein CDI-Event ( javax.enterprise.event.Event ) injiziert. Durch die Typisierung auf IdeaChangedEvent und den selbst definierten @Async -Qualifier können wir innerhalb der Aktoreninstanz ein speziell qualifiziertes CDI-Event feuern. Das effektive Feuern des CDI-Events mit Hilfe der Methode #fire erfolgt synchron. Da die Methode #onReceive selbst jedoch asynchron ausgeführt wird, ist das Feuern des CDI-Events aus der Sicht von EntityChangedEventBroadcaster asynchron und blockiert dessen weitere Ausführung somit nicht. Der Qualifier @Async dient lediglich zur Unterscheidung bzw. Selektion dieses neuen Event-Typs.
public class IdeaChangedEventActor extends UntypedActor {
  private final Event<IdeaChangedEvent> entityChangedEvent;

  @Inject
  public IdeaChangedEventActor(
    @Async Event<IdeaChangedEvent> entityChangedEvent) {

    this.entityChangedEvent = entityChangedEvent;
  }

  @Override
  public void onReceive(Object message) throws Exception {
    if (message instanceof IdeaChangedEvent) {
      this.entityChangedEvent.fire((IdeaChangedEvent)message);
    } else {
      unhandled(message);
    }
  }
}
@Async IdeaChangedEvent repräsentiert ein synchrones CDI-Event, welches in einem asynchronen Aktor-Thread gefeuert wird. Der in Listing CDI Event-Observer für @Async IdeaChangedEvent ersichtliche CDI Event-Observer folgt dementsprechend den herkömmlichen Regeln für CDI-Observer. Der @Async -Qualifier schränkt den Observer nur auf Events ein, die in unserem Fall durch IdeaChangedEventActor gefeuert wurden. Somit wird der Observer im gleichen Thread wie IdeaChangedEventActor ausgeführt, jedoch in einem anderen Thread als EntityChangedEventBroadcaster . Folglich wird auch die Ausführung von EntityProcessorInterceptor bzw. die Abarbeitung des gesamten Web-Requests nicht blockiert.
@ApplicationScoped
public class IdeaHistoryProcessor {
    @Inject
    private ObjectConverter currentObjectConverter;

    @Inject
    private EntityChangeRepository entityChangeRepository;

    public void onIdeaCreated(
      @Observes @Async IdeaChangedEvent changedEvent) {
        Idea entity = changedEvent.getEntity();
        String ideaSnapshot = currentObjectConverter.toString(entity);
        EntityChange entityChange = new EntityChange(
                entity.getId(),
                entity.getVersion() != null ? entity.getVersion() : 0,
                ideaSnapshot,
                changedEvent.getCreationTimestamp());

        entityChangeRepository.save(entityChange);
    }
}
Der nächste Schritt deckt zwei Aspekte ab. In Listing Notifizierung von Akka-Aktoren haben wir einen Injection-Point vom Typ ActorRef verwendet. Da Akka derzeit kein eigenes CDI-Modul hat, benötigen wir einen CDI-Producer für diesen Typ. Wie bereits erwähnt benötigen wir einen zusätzlichen CDI-Qualifier, damit wir im Producer feststellen können für welchen Aktorentyp eine Aktorenreferenz erstellt werden soll. Listing Eigener CDI-Qualifier für Aktoren zeigt eine mögliche Variante eines solchen Qualifiers. Damit wir nicht für jeden konkreten Aktorentyp einen CDI-Producer benötigen, sondern nur einen generischen Producer für alle Injection-Points vom Typ ActorRef , müssen wir alle Annotationmembers, die nicht für die Zuordnung zu einer generischen Producer-Methode benötigt werden, mit @Nonbinding markieren. In unserem Beispiel werten wir alle Annotationmembers in der Producer-Methode selbst aus und müssen daher auch alle mit @Nonbinding annotieren.
@Qualifier
@Retention(RUNTIME)
@Target({METHOD, FIELD})
public @interface Actor {
  String AKKA_DEFAULT = "default";

  @Nonbinding
  Class<? extends akka.actor.Actor> type();

  @Nonbinding
  String systemName() default AKKA_DEFAULT;
}
Statt für jeden Aktoren-Typ, in unserem Fall IdeaChangedEventActor und im nächsten Abschnitt UserRegisteredEventActor , einen separaten CDI-Producer anzulegen, veranschaulicht Listing Producer für Aktorenreferenzen eine generische Producer-Methode für sämtliche Injection-Points vom Typ ActorRef in Kombination mit dem Qualifier @Actor . Die hierfür benötigte Aktorenklasse wird mit @Actor#type erst im CDI-Bean, in unserem aktuellen Beispiel mit @Actor(type = IdeaChangedEventActor.class) , angegeben. Bei den bisherigen CDI-Producern haben wir CDI-Qualifier meistens nur für die Zuordnung von Injection-Point und Producer verwendet. Solche Producer haben dann unabhängig vom Injection-Point eine Instanz erzeugt, welche dem Injection-Point zugeordnet wurde. In unserem aktuellen Beispiel ist dies nicht möglich, weil das Ergebnis des Producers von Metadaten des jeweiligen Injection-Points abhängig ist. Das heißt unsere Producer-Methode muss Metadaten des Injection-Points dynamisch auswerten, um im konkreten Fall die erforderliche Information von @Actor zu erfahren. Anders gesagt müssen wir die Metadaten der Referenzvariablen, in unserem Beispiel ActorRef asyncBroadcaster , auswerten, bevor wir eine Contextual-Instance erzeugen könne, welche vom CDI-Container dieser Referenzvariablen zugewiesen wird. Dieser Ansatz erscheint auf den ersten Blick eventuell unnötig kompliziert, ist aber immer dann erforderlich wenn ein generischer Producer für Injection-Points eines Typs mit unterschiedlichen Metadaten umgesetzt werden soll. CDI definiert für solche Spezialfälle das Interface javax.enterprise.inject.spi.InjectionPoint . Jede Producer-Methode für dependent-scoped Beans kann optional einen Parameter vom Typ InjectionPoint verwenden, um die Definition des jeweiligen Injection-Points auszuwerten. Je Aufruf einer solchen Producer-Methode befüllt der CDI-Container diesen Parameter mit einer InjectionPoint -Instanz, die den aktuellen InjectionPoint repräsentiert.

 

Über Methoden wie bspw. InjectionPoint#getAnnotated#getAnnotation kann auf Metadaten des aktuellen Injection-Points zugegriffen werden. In Listing Producer für Aktorenreferenzen holen wir uns vom Injection-Point die @Actor -Instanz über die eben erwähnte Methode. Diese Instanz der @Actor -Annotation enthält alle Werte die wir beim Injection-Point angegeben haben und für die Erzeugung einer passenden ActorRef benötigen. @Actor#type liefert in unserem Beispiel IdeaChangedEventActor.class zurück und @Actor#systemName den Default-Wert default .
Tipp: Wir wissen, dass @Actor in unserem Beispiel vorhanden ist, da unsere Producer-Methode für alle Injection-Points vom Typ ActorRef zuständig ist, die mit dem @Actor -Qualifier markiert sind.
Mit der soeben extrahierten Information können wir die Erzeugung des Aktorensystems an Akka delegieren. Der hierfür nötige Aufruf von Akka ist in der Methode #getActorSystem gekapselt. Das Ergebnis dieses Aufrufs ist eine Instanz vom Typ akka.actor.ActorSystem , welche für die Erzeugung einer ActorRef verwendet werden kann. Details hierzu sind im Git-Repository von IdeaForkLite ersichtlich. Das von Akka erzeugte Aktorensystem kann in einer Map abgelegt werden. Bei jedem weiteren Zugriff auf das gleiche Aktorensystem wird das bereits erzeugte Aktorensystem herangezogen. Um die Aktorensysteme direkt in einer ActorRefProducer -Instanz für die Laufzeit der Applikation zu cachen, ist diese Klasse mit @ApplicationScoped annotiert.
Tipp: Die Producer-Methode wird neben @Produces auch mit dem Qualifier @Actor annotiert. Da @Actor#type keinen Default-Wert definiert, wie es bei @Actor#systemName der Fall ist, müssen wir einen beliebigen aber gültigen Wert an dieser Stelle angeben, damit es zu keinem Kompilierungsfehler kommt. Der hier angegebene Wert akka.actor.Actor.class wird zur Laufzeit ignoriert, da er nur für den Kompilierungsprozess erforderlich ist.
Der letzte Schritt in der Producer-Methode aus Listing Producer für Aktorenreferenzen ermöglicht die Injizierung von CDI-Beans in Aktor-Instanzen. Handelt es sich um eine Aktor-Implementierung die von UntypedActor ableitet, so wird dem Aktorensystem mitgeteilt, dass CdiAwareCreator zur Erzeugung der Aktoreninstanzen verwendet werden soll. CdiAwareCreator implementiert das Interface akka.japi.Creator und wird mit der gewünschten Aktorenklasse initialisiert. Da wir dependent-scoped Beans als Aktoren verwenden sollen, delegieren wir in der Methode CdiAwareCreator#create an BeanProvider#getDependent#get . Der Methode BeanProvider#getDependent wird die Klasse der zu erzeugenden Aktoren-Instanz übergeben. Als Ergebnis liefert diese Hilfsmethode von DeltaSpike eine Instanz vom Typ org.apache.deltaspike.core.api.provider.DependentProvider . Diese Datenstruktur kapselt die dependent-scoped Instanz zusammen mit den dazugehörigen Metadaten, die für eine spätere Zerstörung der Instanz durch den Aufruf DependentProvider#destroy benötigt werden. Durch den Aufruf der Methode #get stellen wir Akka die dependent-scoped Instanz zur Verfügung.
@ApplicationScoped
public class ActorRefProducer {
  private Map<String, ActorSystem> actorSystemMap =
    new HashMap<String, ActorSystem>();

  @Produces
  @Actor(type = akka.actor.Actor.class)
  protected ActorRef createActorRef(InjectionPoint injectionPoint) {
    final Actor actorQualifier = injectionPoint.getAnnotated()
      .getAnnotation(Actor.class);

    ActorSystem actorSystem = getActorSystem(actorQualifier.systemName());

    if (!UntypedActor.class.isAssignableFrom(actorQualifier.type())) {
      actorSystem.actorOf(Props.create(actorQualifier.type()));
    }
    return actorSystem.actorOf(Props.create(
      new CdiAwareCreator(actorQualifier.type())));
  }

  public ActorSystem getActorSystem(String actorSystemName) {
    ActorSystem actorSystem = actorSystemMap.get(actorSystemName);
    if (actorSystem == null || actorSystem.isTerminated()) {
      actorSystem = bootActorSystem(actorSystemName);
    }

    return actorSystem;
  }

  private synchronized ActorSystem bootActorSystem(
    String actorSystemName) {

    ActorSystem actorSystem = actorSystemMap.get(actorSystemName);
    if (actorSystem != null && !actorSystem.isTerminated()) {
      return actorSystem;
    }

    actorSystem = ActorSystem.create(actorSystemName);
    actorSystemMap.put(actorSystemName, actorSystem);
    return actorSystem;
  }

  @PreDestroy
  protected void cleanup() {
        for (ActorSystem actorSystem : actorSystemMap.values()) {
            if (!actorSystem.isTerminated()) {
                actorSystem.shutdown();
            }
        }
  }

  private static class CdiAwareCreator
    implements Creator<akka.actor.Actor> {

    private static final long serialVersionUID = 3739310463390426896L;

    private final Class<? extends akka.actor.Actor> actorClass;

    public CdiAwareCreator(Class<? extends akka.actor.Actor> actorClass) {
      this.actorClass = actorClass;
    }

    @Override
    public akka.actor.Actor create() throws Exception {
      return BeanProvider.getDependent(actorClass).get();
    }
  }
}
Akka registriert die zur Verfügung gestellte Instanz intern und erzeugt die dazugehörige Instanz vom Typ ActorRef , welche wir schließlich als Ergebnis der Producer-Methode zurückgeben. Producer-Methoden in welchen Informationen des Injection-Points ausgewertet werden, müssen dependent-scoped Beans erzeugen. Da Akka ohnedies kein erweitertes Scope-Konzept hat, müssen wir hier keine zusätzlichen Aspekte bei der Integration beider Container beachten.

 

Die Zerstörung von Aktor-Instanzen übernimmt Akka (derzeit) selbst, wodurch wir nicht wie üblich DependentProvider#destroy aufrufen können. Aus diesem Grund können wir in derart integrierten Aktoren zwar @PostConstruct , aber nicht @PreDestroy verwenden. Würden wir eine Callback-Methode mit @PreDestroy annotieren, dann würde diese nicht aufgerufen werden.

 

Die erzeugten Aktor-Systeme selbst müssen wir allerdings beim Herunterfahren der Applikation manuell beenden. In der @PreDestroy -Callback-Methode namens ActorRefProducer#cleanup rufen wir hierfür die Methode akka.actor.ActorSystem#shutdown auf die jeweilige Instanz auf.

 

Bedingt durch die Integration von Akka und CDI-Beans müssen wir den Scope von MonitoredStorage ändern. Bisher haben wir dieses CDI-Bean mit @RequestScoped dem Request-Context zugeordnet. Allerdings ist der Request-Context in einem Aktor-Thread nicht aktiv und somit müssen wir bspw. auf den @ApplicationScoped zurückgreifen, da dieser immer aktiv ist. Diese Änderung setzt natürlich voraus, dass das Bean thread-safe ist.

 

In unserer CDI-Extension zur Validierung von Applikationsstrukturen können wir eine zusätzliche Überprüfung für Aktoren hinzufügen. Listing Validierung von Actor-Beans zeigt ein Beispiel für eine solche Validierung. In unserem Fall überprüfen wir, ob alle Aktor-Klassen explizit oder implizit als dependent-scoped Beans umgesetzt sind.
public class AppStructureValidationExtension implements Extension {
  private List<String> violations = new ArrayList<String>();

  public void validateArtifacts(@Observes ProcessManagedBean pmb,
                                BeanManager beanManager) {
    Class beanClass = pmb.getAnnotatedBeanClass().getJavaClass();
    //...

    if (Actor.class.isAssignableFrom(beanClass)) {
      validateActor(beanClass,
        pmb.getAnnotatedBeanClass().getAnnotations(),
        beanManager);
    }
  }

  public void checkAndAddViolations(
    @Observes AfterDeploymentValidation afterDeploymentValidation) {

    //...
  }

  private void validateActor(Class beanClass, Set<Annotation> annotations,
                             BeanManager beanManager) {
    for (Annotation annotation : annotations) {
      if (beanManager.isScope(annotation.annotationType()) &&
        !Dependent.class.isAssignableFrom(annotation.annotationType())) {

        this.violations.add(
          "It isn't allowed to use " + annotation.annotationType() +
          " for Akka-Actors. " +
          "Please remove the annotation from " + beanClass.getName());
      }
    }
  }
}

 

Im zweiten Abschnitt dieses Kapitels integrieren wir CDI und Spring. In den dazugehörigen Beispielen bauen wir darüber hinaus auf der Integration mit Akka auf.

7.2 Beans aus anderen Welten

Im Laufe der Zeit entstanden verschiedene Dependency-Injection Frameworks. Einige dienten der CDI Expert-Group bei der Definition von CDI sogar direkt oder indirekt als Inspiration. Während bspw. JBoss Seam durch CDI und Apache DeltaSpike abgelöst worden ist, existieren Projekte wie Google Guice und das Spring-Framework parallel zu CDI weiter. Der Funktionsumfang ist bei einigen der verfügbaren Alternativen ähnlich. CDI wird in vielen Java EE Applikationen gerne eingesetzt, da CDI seit Java EE 6 ein Bestandteil der Plattform ist und im Vergleich zu den Alternativen kaum Wünsche offen lässt.

 

So ähnlich die einzelnen Dependency-Injection Frameworks sind, so unterschiedlich sind die dazugehörigen Ökosysteme. Vor allem das Spring-Framework hat durch seine lange Historie sehr viele Erweiterungen. Wie sinnvoll bzw. erforderlich die einzelnen Erweiterungen sind hängt sehr stark vom Einsatzgebiet ab. In vielen Fällen stellt das Spring-Framework Adapter für andere Technologien zur Verfügung, um eine etwas einfachere Verwendung dieser Technologien zu ermöglichen. Aus Sicht einer Java EE Applikation kann es folglich sinnvoll erscheinen solche Vorteile zu nutzen, wenn es keine äquivalente Erweiterung für CDI gibt. Dies ist jedoch kein Grund eine CDI-basierte Applikation vollständig auf ein anderes Framework wie Spring umzustellen. Wie zuvor bei der Integration mit Akka können wir auch hier durch die starke Erweiterbarkeit von CDI die Vorteile beider Technologien nutzen. Wir können eine portable Erweiterung implementieren, die es ermöglicht Spring-Beans in CDI-Beans zu nutzen. Eine solche Integration ist weitläufig als CDI/Spring-Bridge bekannt. Im Laufe der Jahre sind in der CDI-Community verschiedene Bridges entstanden. Der überwiegende Teil davon ist unidirektional, aber auch bidirektionale Umsetzungen sind mit Einschränkungen möglich.

 

In IdeaForkLite möchten wir eine rudimentäre Spring-Bridge umsetzen, mit welcher eine unidirektionale Injizierung von Spring-Beans in CDI-Beans ermöglicht werden soll. Über eine solche Bridge sind anschließend Spring-Erweiterungen in einer primär CDI-basierten Applikation, wie es IdeaForkLite ist, verwendbar. Konkret werden wir IdeaForkLite mit Spring-Mail erweitern. Abgesehen davon können solche Bridges auch für die schrittweise Migration von Spring nach CDI verwendet werden.
Tipp: Bidirektionale CDI/Spring-Bridges erlauben neben der Injizierung von Spring-Beans in CDI-Beans auch die Injizierung von CDI-Beans in Spring-Beans. Sowohl bei unidirektionalen als auch bei bidirektionalen Bridges muss jedoch ein primärer Container definiert werden. Dieser wird als erster gestartet und ist dafür verantwortlich den Startvorgang des jeweils anderen Containers zu initialisieren.
Ausgehend von einer einfachen Basiskonfiguration für Spring werden wir mit Hilfe einer portablen CDI Erweiterung den Spring-Container starten und für jedes Spring-Bean einen entsprechenden Adapter als CDI-Bean registrieren, welcher bei der Erzeugung einer Contextual-Instance den Aufruf an den Spring-Container weiterleitet. Listing Spring via CDI-Extension starten zeigt das Grundgerüst für eine CDI-Extension Klasse namens SpringBridgeExtension . Diese Extension-Klasse muss, wie bei CDI üblich, in der Service-Loader Konfigurationsdatei META-INF/services/javax.enterprise.inject.spi.Extension aktiviert werden. Nach dem erfolgreichen Start des Spring-Containers in der Observer-Methode für das AfterBeanDiscovery -Event können wir die erzeugte ConfigurableApplicationContext -Instanz einer Instanzvariablen der Extension zuweisen. In Listing Spring via CDI-Extension starten wird diese Variable springContext genannt. Da der CDI-Container je Applikation und Extension-Klasse nur eine Instanz erzeugt, können wir in verschiedenen Phasen des Container-Lifecycles auf den abgelegten Spring-Context zugreifen. Im AfterBeanDiscovery -Observer können wir die Spring-Bean-Definitionen über die Bean-Factory auswerten. Allerdings sind wir nur an selbst erstellten Spring-Beans interessiert, welche wir in unserer Applikation umsetzten. Mit der Methode AfterBeanDiscovery#addBean fügen wir somit nur für jene Spring-Beans CDI-Adapter-Beans hinzu, deren Packages nicht mit org.springframework beginnen.
Tipp: Vor allem in älteren EE Servern kann es vorkommen, dass Instanzvariablen in Extension-Klassen, in unserem Fall springContext , nur je Observer-Methode verwendbar sind. In solchen Fällen muss auf Klassenvariablen zurückgegriffen werden. Die einzige Alternative wäre ein Server-Update.
public class SpringBridgeExtension implements Extension {
  private ConfigurableApplicationContext springContext;

  public void initContainerBridge(
    @Observes AfterBeanDiscovery abd, BeanManager beanManager) {

    springContext = bootContainer();

    for (String beanName : springContext.getBeanDefinitionNames()) {
      BeanDefinition beanDefinition = springContext.getBeanFactory()
        .getBeanDefinition(beanName);
      Class<?> beanClass = springContext.getType(beanName);

      if (!beanClass.getName().startsWith("org.springframework.")) {
        abd.addBean(createBeanAdapter(
          beanClass, beanName, beanDefinition,
          springContext, beanManager));
      }
    }
  }

  public void cleanup(@Observes BeforeShutdown beforeShutdown) {
    springContext.close();
  }

  private <T> Bean<T> createBeanAdapter(
    Class<T> beanClass, String beanName,
    BeanDefinition beanDefinition,
    ConfigurableApplicationContext applicationContext,
    BeanManager bm) {

    //...
  }

  private ConfigurableApplicationContext bootContainer() {
    //...
  }
}
Die Implementierung der Methode #createBeanAdapter aus Listing CDI Bean-Adapter für Spring-Beans erzeugen veranschaulicht, dass auch hier DeltaSpike hilfreiche Konzepte zur Verfügung stellt. Mit AnnotatedTypeBuilder kann auf Basis von beliebigen Klassen eine AnnotatedType -Instanz erzeugt werden. In unserem Beispiel könnten wir hierfür auch BeanManager#createAnnotatedType verwenden. Allerdings könnten wir mit AnnotatedTypeBuilder das erzeugte Ergebnis beeinflussen, um bspw. Bean-Metadaten hinzuzufügen oder zu entfernen. Einen vergleichbaren Builder namens BeanBuilder gibt es auch für javax.enterprise.inject.spi.Bean . Mit diesem Builder verändern wir in Listing CDI Bean-Adapter für Spring-Beans erzeugen verschiedene Eigenschaften, damit der Adapter für das Spring-Bean möglichst effizient funktioniert. Beispielsweise verwenden wir als Scope-Annotation in jedem Fall @Dependent , da der Spring-Container für den Scope der Spring-Beans verantwortlich ist. Die vom Spring-Context herausgegebene Referenz auf ein Spring-Bean wird somit unverändert weiterverwendet. Der Bean-Name wird in unserem Beispiel ebenfalls von Spring übernommen. Da der Spring-Container die Injection-Points von Spring-Beans befüllt, können wir in einem weiteren Schritt mit der Methode #injectionPoints sämtliche Injection-Points aus Sicht von CDI entfernen. Bis zu diesem Punkt erzeugen wir allerdings nur einen leeren Adapter. Erst durch die Implementierung und Registrierung einer ContextualLifecycle -Instanz können wir bei der Erzeugung des CDI-Beans an den Spring-Context delegieren. In unserem Fall nennen wir die Implementierung SpringAwareBeanLifecycle .
private <T> Bean<T> createBeanAdapter(
  Class<T> beanClass, String beanName,
  BeanDefinition beanDefinition,
  ConfigurableApplicationContext applicationContext, BeanManager bm) {

  String beanScope = beanDefinition.getScope();
  ContextualLifecycle lifecycleAdapter =
    new SpringAwareBeanLifecycle(applicationContext, beanName, beanScope);

   beanClass = ProxyUtils.getUnproxiedClass(beanClass);
    return new BeanBuilder<T>(bm)
      .readFromType(new AnnotatedTypeBuilder<T>()
        .readFromType(beanClass).create())
      .name(beanName)
      .beanLifecycle(lifecycleAdapter)
      .injectionPoints(Collections.<InjectionPoint>emptySet())
      .scope(Dependent.class)
      .create();
}
SpringAwareBeanLifecycle aus Listing Contextual-Lifecycle für Spring-Beans delegiert die Verwaltung der Bean-Instanzen mit Hilfe der Methode #getBean an den Spring-Context. Für den CDI-Container ist das Ergebnis ein einfaches dependent-scoped Bean. Entsprechend werden auch die Regeln für dependent-scoped Beans umgesetzt. Der äquivalente Spring-Scope wird als Prototype-Scope bezeichnet. Somit dürfen wir bei der Zerstörung der Contextual-Instance durch den CDI-Container den Aufruf nur an die Methode #destroyBean weiterleiten, wenn es sich um den Prototype-Scope handelt. Anderenfalls muss der Spring-Container die Lebensdauer des entsprechenden Spring-Beans abhängig vom Scope des Beans verwalten.
class SpringAwareBeanLifecycle implements ContextualLifecycle {
  private final ConfigurableApplicationContext applicationContext;
  private final String beanName;
  private final boolean prototypeScope;

  public SpringAwareBeanLifecycle(
    ConfigurableApplicationContext applicationContext,
    String beanName, String scope) {

    this.applicationContext = applicationContext;
    this.beanName = beanName;
    this.prototypeScope = "prototype".equalsIgnoreCase(scope);
  }

  @Override
  public Object create(Bean bean, CreationalContext creationalContext) {
    return this.applicationContext.getBean(this.beanName);
  }

  @Override
  public void destroy(Bean bean, Object instance,
                      CreationalContext creationalContext) {

    if (this.prototypeScope) {
      this.applicationContext.getBeanFactory()
        .destroyBean(this.beanName, instance);
    }
  }
}
Unsere rudimentäre CDI/Spring-Bridge ist hiermit fertiggestellt und wir können unser erstes Spring-Bean implementieren, welches wir in einem weiteren Schritt in ein CDI-Bean injizieren. Wie eingangs erwähnt werden wir zu Demonstrationszwecken Spring-Mail verwenden. In der Praxis ist es natürlich naheliegender Alternativen wie bspw. Apache Commons Email zu evaluieren, da hierfür kein zusätzlicher Container erforderlich ist.

 

Listing Einfaches Spring-Bean zeigt eine simple Implementierung eines Spring-Beans, welches das effektive Versenden an JavaMailSender#send von Spring-Mail delegiert. Da wir nur ein Spring-Bean in IdeaForkLite implementieren, legen wir kein eigenes Modul an. Aus diesem Grund sieht auch der CDI-Container dieses Bean. Jedoch bekommt das CDI-Adapter-Bean für dieses Spring-Bean den gleichen Typ. Somit müssen wir diese Klasse für den CDI-Container unsichtbar machen. In unserem Beispiel greifen wir hierfür auf @Exclude von DeltaSpike zurück. Dies wäre natürlich nicht erforderlich, wenn wir Spring-Beans außerhalb eines BDAs für CDI bereitstellen.
Tipp: Einen ähnlichen Aspekt müssen wir auch aus der Sicht vom Spring-Container beachten. Würde der Spring-Container alle CDI-Beans von IdeaForkLite sehen, dann würden wir Adapter-Beans für Spring-Beans registrieren, die bereits CDI-Beans sind und nur für den CDI-Container sichtbar sein sollen. Für diese etwas kompliziert klingende zyklische Konstellation gibt es eine einfache Lösung. Hierfür müssen wir in IdeaForkLite , neben Konfigurationen für Spring-Mail, das Basis-Package für Spring-Beans unserer Applikation in der Spring-Konfigurationsdatei namens applicationContext.xml definieren.
@Exclude
public class SpringMailSender {
  @Autowired
  private JavaMailSender mailSender;

  public void send(String senderAddress, String recipientAddress,
                   String subject, String text) {
    SimpleMailMessage message = new SimpleMailMessage();
    message.setTo(recipientAddress);
    message.setFrom(senderAddress);
    message.setSubject(subject);
    message.setText(text);
    this.mailSender.send(message);
  }
}
Das Spring-Bean aus Listing Einfaches Spring-Bean können wir im nächsten Schritt in ein CDI-Bean namens MailService injizieren. Listing Injizierung eines Spring-Beans in ein CDI-Bean verdeutlicht, dass sich der Injection-Point nicht von einem regulären Injection-Point für ein CDI-Bean unterscheidet. Das zuvor implementierte CDI-Adapter-Bean wird zur Laufzeit aufgerufen, wenn der CDI-Container den Injection-Point in MailService befüllen möchte.
@ApplicationScoped
public class MailService {
  @Inject
  private SpringMailSender mailSender;

  public void sendWelcomeMessage(User user) {
    String senderAddress = ConfigResolver
      .getProjectStageAwarePropertyValue(
        "ideafork.sender", "admin@ideafork.com");
    String subject = "Welcome " + user.getNickName();
    String text = "Welcome @ IdeaFork!";
    this.mailSender.send(senderAddress, user.getEmail(), subject, text);
  }
}
Auf Basis dieser Vorarbeiten können wir in IdeaForkLite einen weiteren Aktor hinzufügen, über welchen asynchron E-Mails nach einer erfolgreichen Registrierung versendet werden können. Wir könnten auch hier wieder im asynchronen Aktor-Thread ein synchrones CDI-Event mit dem @Async -Qualifier triggern. Für unseren Anwendungsfall ist dies nicht erforderlich und folglich delegieren wir im Listing Mails asynchron senden direkt an das zuvor umgesetzte MailService . Außerdem benötigen wir ein Event namens UserRegisteredEvent , welches im gleichen Listing dargestellt ist.
public class UserRegisteredEvent extends EntityChangedEvent<User> {
    public UserRegisteredEvent(User createdEntity) {
        super(createdEntity);
    }
}

public class UserRegisteredEventActor extends UntypedActor {
  private final MailService mailService;

  @Inject
  public UserRegisteredEventActor(MailService mailService) {
    this.mailService = mailService;
  }

  @Override
  public void onReceive(Object message) throws Exception {
    if (message instanceof UserRegisteredEvent) {
      this.mailService.sendWelcomeMessage(((UserRegisteredEvent)message)
        .getEntity());
    } else {
      unhandled(message);
    }
  }
}
Via Constructor-Injection können wir unser MailService -Bean injizieren, um in der Methode #onReceive mit Hilfe dieses CDI-Beans und dem dahinterliegenden Spring-Bean E-Mails zu versenden. In unserem Beispiel wollen wir eine Willkommensnachricht versenden nachdem eine neue User-Entität gespeichert wurde.
Die Registrierungslogik befindet sich in IdeaForkLite in der Methode UserService#registerUser . Der in Listing CDI- und Akka-Events triggern veranschaulichte Ausschnitt zeigt neben der erweiterten Methode auch den neuen UserRegisteredEventBroadcaster , welcher zuerst ein asynchrones Actor-Event über Akka und anschließend ein synchrones CDI-Event triggert.
@Service
public class UserService {
  //...

  @Inject
  private UserRegisteredEventBroadcaster userRegisteredEventBroadcaster;

  public User registerUser(User newUser) {
    if (userRepository.loadByEmail(newUser.getEmail()) == null) {
      newUser.setPassword(
        passwordManager.createPasswordHash(newUser.getPassword()));
      userRepository.save(newUser);
      User registeredUser = userRepository.findBy(newUser.getId());

      if (registeredUser != null) {
        userRegisteredEventBroadcaster
          .broadcastUserRegisteredEvent(registeredUser);
        return registeredUser;
      }
    }
    return null;
  }
}

@TransactionScoped
public class UserRegisteredEventBroadcaster {
    @Inject
    @Default
    private Event<UserRegisteredEvent> userRegisteredEvent;

    @Inject
    @Actor(type = UserRegisteredEventActor.class)
    private ActorRef asyncBroadcaster;

    public void broadcastUserRegisteredEvent(User entity) {
        UserRegisteredEvent userRegisteredEvent =
          new UserRegisteredEvent(entity);
        asyncBroadcaster.tell(userRegisteredEvent, this.asyncBroadcaster);
        userRegisteredEvent.fire(userRegisteredEvent);
    }
}
Die bisher beschriebene Umsetzung funktioniert einwandfrei, hat jedoch vor allem bei Unit-Tests den Nachteil, dass bei der Durchführung einer Registrierung immer versucht wird ein E-Mail zu verschicken. Stattdessen würde es uns genügen zu überprüfen, ob ein E-Mail versendet werden würde bzw. ob der Inhalt korrekt ist. Listing Mail-Service für Unit-Tests zeigt wie wir den ersten Fall ermöglichen können, indem wir im Test-Package ein spezialisiertes CDI-Bean erstellen und die Methode #sendWelcomeMessage überschreiben. Da wir dieses CDI-Bean nur im Test-Package verwenden ist im produktiven Package von IdeaForkLite immer die ursprüngliche MailService -Implementierung aktiv, weil es dort die Klasse TestMailService nicht gibt. Falls wir TestMailService für Integration-Tests ebenfalls nicht verwenden wollen, dann könnten wir bspw. die Klasse zusätzlich mit @Exclude(ifProjectStage = ProjectStage.IntegrationTest.class) annotieren.
@Specializes
public class TestMailService extends MailService {
    private AtomicInteger sentWelcomeMessageCount = new AtomicInteger();

    @Override
    public void sendWelcomeMessage(User user) {
        sentWelcomeMessageCount.incrementAndGet();
    }

    public Integer getSentWelcomeMessageCount() {
        int result = sentWelcomeMessageCount.get();
        if (result == 0) {
            return null;
        }
        return result;
    }
}

 

Schließlich können wir in unserem JUnit-Test das TestMailService -Bean injizieren, um das Ergebnis nach einem erfolgreichen Registrierungsversuch zu überprüfen. Da wir E-Mails immer über einen asynchronen Aktor-Thread versenden, müssen wir eine Verzögerung einbauen. Hierfür gibt es mehrere Realisierungsmöglichkeiten. In Listing Test mit spezialisiertem Mail-Service ist dies mit einem RetryHelper umgesetzt. Die vollständige Implementierung ist im Git-Repository von IdeaForkLite ersichtlich.
@RunWith(CdiTestRunner.class)
public class IdeaForkBaseFlowTest {
  @Inject
  private RegistrationViewCtrl registrationViewCtrl;

  @Inject
  private TestMailService testMailService;

  @Inject
  private EntityChangeRepository entityChangeRepository;

  //...

  @Test
  public void flowFromRegistrationToIdeaPromotion() {
    registrationViewCtrl.getNewUser().setNickName("os890");
    registrationViewCtrl.getNewUser().setEmail("os890@test.org");
    registrationViewCtrl.getNewUser().setPassword("test");
    Class<? extends ViewConfig> navigationResult =
      registrationViewCtrl.register();

    //...

    int sentWelcomeMessageCount = getSentWelcomeMessageCount();
    Assert.assertEquals(1, sentWelcomeMessageCount);
  }

  private Integer getSentWelcomeMessageCount() {
    return new RetryHelper<Integer>() {
      @Override
      protected Integer execute() {
        Integer result = testMailService.getSentWelcomeMessageCount();

        if (result == null) {
          return 0;
        }
        return result;
      }
    }.start();
  }

  private abstract class RetryHelper<T> {
    //...
  }
}

 

Ob die hier beschriebene Integration von CDI und Spring sinnvoll ist hängt stark von den Anforderungen in einem Projekt ab. In unserem Fall hätten wir bspw. auf Spring verzichten können. Statt Spring-Mail könnten wir Alternativen wie bspw. Apache Commons Email verwenden. Daher sollte in der Evaluierungsphase geprüft werden, ob die Nachteile eines zusätzlichen Containers im Vergleich zum Nutzen der Add-ons akzeptabel sind. Zu solchen Nachteilen zählen unter anderem eine höhere Komplexität in der Applikation durch die Verwendung eines weiteren Containers, dessen Konfiguration und Integration. Außerdem entsteht zur Laufzeit selbst nach dem etwas längerem Applikationsstart ein gewisser Overhead und der etwas höhere Speicherbedarf sollte ebenfalls nicht missachtet werden. In manchen Fällen überwiegen jedoch die Vorteile und somit können CDI basierte Applikationen mit solchen Bridges von anderen Ökosystemen profitieren.