Zustellung von CDI-Events testen
 
  Zustellung von bedingten CDI-Events testen
 
  Verwendung von Scopes
 
  Verwendung von Qualifiern mit Annotation-Attribut
 
  Verwendung von Annotation Literal mit Instance
 
  Verwendung von Annotation Literal mit BeanManager
 
  Verwendung einer eigenen Stereotyp-Annotation
 
  Verwendung der Interceptor-Strategy
 
  Verwendung der Interceptor-Annotation
 
  Unit-Test mit Method-Injection
 
  Unit-Test mit Field-Injection
 
  Test-Observer
 
  Spezialisiertes Bean mit Producer und Disposer
 
  Spezialisierte Implementierungen
 
  Repository Refactoring in IdeaFork
 
  Qualifier mit Annotation-Attribut
 
  Producer-Methode
 
  Observer zur Speicherung von Revisionen
 
  Manueller Lookup via BeanManager
 
  Manueller Field- und Method-Injection Trick
 
  Kontrolle der Interceptor-Test-Strategy
 
  Konfigurierbare Default-Implementierung
 
  Klasse für eine typsichere Konfiguration
 
  Interceptor-Test-Strategy
 
  Interceptor-Strategy
 
  Interceptor-Implementierung
 
  Interceptor-Annotation
 
  Interceptor mit Zusatzinformation
 
  Injection-Point mit Qualifier
 
  Idea Entität
 
  Generische Decorator Logik
 
  Explizite Typisierung
 
  Explizite Event-Klasse
 
  Exkludiertes Annotation-Attribut
 
  Erweiterte ApplicationConfig
 
  Ergebnis der Observer-Logik überprüfen
 
  Einfache Selektion via Producer
 
  Einfache Qualifier-Annotation als Marker
 
  Einfache Auswertung von Interceptor-Informationen
 
  Eigene Stereotyp-Annotation
 
  Dynamische Injizierung mit mehreren Beans
 
  Dynamische Injizierung mit mehreren Beans
 
  Dynamische Injizierung mit Instance
 
  Dynamische Injizierung für optionale Beans
 
  Disposer-Methode
 
  Decorator Implementierung
 
  Constructor-Injection
 
  CDI-Events überwachen
 
  CDI-Events feuern
 
  CDI-Bean als einfache Factory
 
  Bedingter Test-Observer
 
  Bean-Klasse mit Qualifier
 
  Basis-Entität in IdeaFork
 
  Annotation Literal für ExternalFormat
 
  Alternative Stereotyp-Annotation
 
  Alternative Implementierung
 
  Aktivierung einer Interceptor-Implementierung
 
  Aktivierung einer Decorator-Implementierung
 
  Aktivierung einer alternativen Implementierung
 
  Aktivierung alternativer Beans via Stereotyp
 
  Abstrakter Decorator

2 CDI Grundkonzepte

In diesem Kapitel betrachten wir die grundlegenden Konzepte von CDI. Am Ende des Kapitels kennen Sie die wichtigsten Basisfunktionalitäten, welche zur Entwicklung von CDI-basierten Applikationen verwendet werden können. CDI bietet darüber hinaus einige Erweiterungsmöglichkeiten, welche einen zentralen Aspekt dieses Komponentenmodells darstellen. Daher widmen wir diesem Thema ein eigenes Kapitel.

2.1 Typsichere Dependency-Injection

Der wohl bekannteste Bestandteil von CDI ist die typsichere Dependency-Injection. Ein einfaches Beispiel haben wir bereits im Kapitel Einführung in CDI kennengelernt. Dieses Beispiel ist sehr einfach gehalten, repräsentiert jedoch einen der Hauptanwendungsfälle und verbirgt interessante Vorteile. In diesem Kapitel werden wir darüber hinaus weitere Injection-Varianten und deren Eigenschaften kennenlernen.

 

Es gibt mehrere Möglichkeiten, wie Sie dem CDI-Container mitteilen, dass eine Contextual-Reference gesetzt werden soll. Die einfachste Variante ist die "direkte" Injection. Hierbei werden Felder, Methoden oder auch ein Konstruktor mit @Inject annotiert. Dieser Ansatz ist nicht nur einfach anzuwenden, sondern bietet außerdem den Vorteil, dass der CDI-Container während des Startprozesses die Injection-Points validiert. Alle drei Möglichkeiten können beliebig kombiniert werden. Dennoch gibt es eine definierte Reihenfolge, in der die Injizierung durchgeführt wird. Zuerst wird immer die sog. Constructor-Injection ausgeführt. Danach werden alle mit @Inject annotierten Felder befüllt. Dies wird Field-Injection genannt. In einem letzten Schritt wird Method-Injection durchgeführt. Hierbei kann es sich um Initialisierungsmethoden mit beliebig vielen Parameter handeln. Wie bei der Constructor-Injection stellt jeder Parameter einen eigenen Injection-Point dar, welcher durch den CDI-Container befüllt wird.

 

Sollte kein gültiges Managed-Bean für einen Injection-Point gefunden werden, dann wird eine UnsatisfiedResolutionException geworfen. Sollten hingegen mehrere gültige Kandidaten gefunden werden, so wird eine AmbiguousResolutionException geworfen. Die AmbiguousResolutionException werden wir uns später in Verbindung mit Qualifiern noch genauer ansehen. In beiden Fällen wird der Containerstart abgebrochen und somit erkennen Sie frühzeitig, ob es ein grundlegendes Problem in Ihrer Applikation gibt.

 

Die im Kapitel Die Applikation zu diesem Buch beschriebene Beispielapplikation namens IdeaFork benötigt eine zentrale Entität, welcher wir den naheliegenden Namen Idea vergeben. Äquivalent zu Listing 1.2 könnten wir eine Instanz der Klasse einfach in unsere Beans injecten, sofern alle CDI-Regeln erfüllt sind. Dies wird weitläufig als fachliche Injizierung (aka Business-Injection) bezeichnet.
Tipp: Bei fachlicher Injizierung muss mit großer Sorgfalt vorgegangen werden. Fügen wir später bspw. JPA hinzu ist es wichtig, dass eine Instanz nur von einem Container verwaltet wird. In diesem Bereich gibt es viele Empfehlungen für die Strukturierung einer Applikation. Wir folgen der Empfehlung, dass Entitäten nicht von CDI verwaltet werden. Da wir dennoch die Erzeugung von Idea -Instanzen an einer zentralen Stelle durchführen wollen, führen wir einen IdeaManager ein, welcher später zusätzliche Aufgaben erfüllen wird.

2.1.0.1 Field-Injection

Listing Idea Entität zeigt Details der Entität Idea . Vorerst handelt es sich um ein einfaches POJO mit 3 Properties, von denen 2 für die Erzeugung einer Instanz erforderlich sind.
public class Idea {
  private String topic;
  private String category;
  private String description;

  Idea(String topic, String category) {
    this.topic = topic;
    this.category = category;
  }
  //+ Getter- und Setter-Methoden
}
Da wir hier durch den Konstruktor die Erzeugung nur innerhalb des gleichen Pakets zulassen, legen wir die Klasse IdeaManager im gleichen Paket an. Listing CDI-Bean als einfache Factory zeigt die Implementierung von IdeaManager . Vorerst verzichten wir auf die explizite Definition eines Scopes.
public class IdeaManager {
  public Idea createIdeaFor(String topic, String category) {
    return new Idea(topic, category);
  }
}
Mit Listing Idea Entität und CDI-Bean als einfache Factory haben wir alle Bestandteile für die Erstellung eines Unit-Tests, um die Erzeugung einer Idea -Instanz zu testen. In diesem Test (siehe Listing Unit-Test mit Field-Injection ) legen wir ein privates Feld vom Typ IdeaManager an und annotieren es mit @Inject , wodurch wir für CDI einen Injection-Point definieren. Zur Laufzeit befüllt der CDI-Container das Feld (den Injection-Point) mit einer Contextual-Reference, über welche auf die Contextual-Instance zugegriffen werden kann.
@RunWith(CdiTestRunner.class)
public class FieldInjectionTest {
  @Inject
  private IdeaManager ideaManager;

  @Test
  public void ideaCreation() {
    String topic = "Learn CDI";
    String category = "Education";
    String description = "Hello Injection!";

    Idea newIdea = this.ideaManager.createIdeaFor(topic, category);
    newIdea.setDescription(description);

    Assert.assertEquals(topic, newIdea.getTopic());
    Assert.assertEquals(category, newIdea.getCategory());
    Assert.assertEquals(description, newIdea.getDescription());
  }
}
Diesen ersten Schritt könnten wir auch ohne Dependency-Injection umsetzen. Statt der Annotation @Inject müssten wir das Schlüsselwort new für eine manuelle Instanziierung verwenden. Manuell erzeugte Instanzen sind weiterhin möglich, jedoch werden sie nicht durch den CDI-Container verwaltet und somit würden viele CDI basierte Funktionalitäten nicht oder nur über Umwege verwendbar sein. Im nachfolgenden Schritt werden wir den ersten Vorteil von verwalteten Instanzen kennenlernen.
Tipp: Der zuvor erwähnte Vorteil, dass die demonstrierte (direkte) Field-Injection beim Containerstart validiert wird, verhindert gleichzeitig die Verwendung von optionalen Beans. Ein Beispiel hierfür ist die optionale Implementierung eines Interfaces. Zur Lösung solcher Anwendungsfälle benötigen wir eine Indirektion, für welche es mehrere Möglichkeiten gibt. Details zu solchen programmatischen Lookups werden wir in den Kapiteln [ Indirektion mit Instance] und [ Beans via BeanManager finden] behandeln.

2.1.0.2 Constructor-Injection

Eine Alternative zur Field-Injection ist die Constructor-Injection. Hierbei wird ein parametrisierter Konstruktor mit @Inject annotiert. Sobald ein Proxy zur Referenzierung einer Contextual-Instance erforderlich ist, muss ein Default-Konstruktor angelegt werden. Anderenfalls wird während dem Containerstart eine UnproxyableResolutionException geworfen.

 

In unserer Applikation können wir einen IdeaValidator hinzufügen, welcher via Constructor-Injection in IdeaManager gesetzt wird.
public class IdeaManager {
  private final IdeaValidator ideaValidator;

  @Inject
  protected IdeaManager(IdeaValidator ideaValidator) {
    this.ideaValidator = ideaValidator;
  }

  public Idea createIdeaFor(String topic, String category) {
    Idea result = new Idea(topic, category);
    if (!ideaValidator.checkIdea(result)) {
      throw new IllegalArgumentException(
        "Please try it harder next time!");
    }
    return result;
  }
}
Aus technischer Sicht gibt es in CDI keine Vorteile von Constructor-Injection im Vergleich zu den anderen Varianten, da erst vollständig initialisierte Beans vom Container nach außen gegeben werden. In vielen Fällen wird Constructor-Injection primär verwendet, wenn der Injection-Point nicht direkt einer Instanzvariablen zugewiesen wird. Stattdessen wird oft nur ein Wert über den Injection-Point abgefragt und dieser einer Instanzvariablen zugewiesen oder im Konstruktor direkt ausgewertet.
Tipp: In Listing Constructor-Injection ist kein Default-Konstruktor erforderlich. Dies ändert sich sobald ein Bean normal-scoped ist oder einen Interceptor/Decorator hat.

2.1.0.3 Method-Injection

Ähnlich der Constructor-Injection können auch Methoden mit @Inject annotiert werden. Jeder Parameter stellt einen Injection-Point dar, welcher vom CDI-Container injiziert wird. Da Parameter optional sind, können 0-n Parameter angegeben werden. Init-Methoden ohne Parameter werden jedoch üblicherweise mit @PostConstruct annotiert. Für die Contextual-Instance macht es technisch gesehen keinen relevanten Unterschied, allerdings ist @PostConstruct aussagekräftiger und wird genauso wie @PreDestroy von CDI unterstützt. Wie eingangs erwähnt wird Method-Injection nach Constructor- und Field-Injection durchgeführt und stellt den letzten Injizierungsschritt dar.

 

In Listing Unit-Test mit Method-Injection wird statt Field-Injection das eben beschriebene Method-Injection Konzept angewendet.
@RunWith(CdiTestRunner.class)
public class MethodInjectionTest {
  private IdeaManager ideaManager;

  @Inject
  protected void init(IdeaManager ideaManager) {
    this.ideaManager = ideaManager;
  }

  @Test(expected = IllegalArgumentException.class)
  public void invalidIdeaCreation() {
    this.ideaManager.createIdeaFor(null, null);
  }

  //...
}

2.2 Normal- und Pseudo-Scopes

Wie im Kapitel Wichtige Begriffe der Spezifikation beschrieben, werden Contextual-Instances auf Basis der zugrundeliegenden Bean-Metadaten erzeugt. Ein Bestandteil dieser Metadaten ist der Scope des Beans, welcher die Lebensdauer der Contextual-Instance definiert. Ist eine Contextual-Instance noch nicht verfügbar, so wird diese üblicherweise autom. erzeugt, sofern der Scope aktiv ist. Das genaue Verhalten wird durch den Scope und dessen Kontext-Implementierung definiert. Selbst in der CDI-Spezifikation gibt es mit dem Conversation-Scope einen Spezialfall, da dieser explizit gestartet werden muss.

 

Dies ist nicht die einzige Ausnahme in der CDI-Spezifikation. Es wird grundsätzlich zwischen "Pseudo-Scopes" und "Normal-Scopes" unterschieden, wobei Pseudo-Scopes eher selten vorkommen. Wie dieses Kapitel zeigt, folgen beide Scope-Arten unterschiedlichen Regeln. Die Verwendung ist hingegen durchgängig. Sie wählen den passenden Scope für ein Managed-Bean aus und annotieren die Bean-Klasse (oder Producer) mit der entsprechenden Scope-Annotation.

2.2.0.1 Pseudo-Scopes

Bereits JSR-330 enthält eine Annotation ( @Scope ), mit welcher eigene Scope-Annotationen erstellt werden können. In der CDI-Spezifikation werden entsprechende Scopes unter dem Namen "Pseudo-Scope" geführt, da @Scope praktisch undefiniert ist. Dies ist der Grund, warum es zwei ähnliche Scopes ( @javax.inject.Singleton von JSR-330 und @javax.enterprise.context.ApplicationScoped von CDI) gibt. Selbst bei dem wesentlich konkreter definierten @ApplicationScoped gibt es noch Feinheiten, welche zu unterschiedlichen Interpretationen führten.

2.2.0.2 Normal-Scopes

Mit Passivierung und den damit verbundenen Regeln definiert die CDI-Spezifikation einen sehr praktischen Mechanismus. @Scope von JSR-330 ist allerdings zu simpel gehalten und somit fehlt eine Möglichkeit die Passivierung anzugeben. Eine zusätzliche Annotation hätte diese Einschränkung überwunden. Die CDI Expert-Group hat sich hingegen dazu entschlossen eine besser definierte Scope-Art einzuführen, welche es ermöglicht die Passivierbarkeit des Scopes anzugeben. Diese Scope-Art wird "Normal-Scope" genannt. Dementsprechend werden Scope-Annotationen mit @NormalScope annotiert.

 

Ein zentraler Unterschied zwischen beiden Scope-Arten ist, dass die Contextual-Reference auf eine normal-scoped Contextual-Instance immer ein Proxy ist. Hingegen ist dies bei pseudo-scoped Beans nur erforderlich, sobald ein Interceptor/Decorator für das entsprechende Bean definiert ist, da dies technisch nur mit angepasstem Bytecode möglich ist. Wie bereits im Kapitel Einführung in CDI erwähnt, sind normal-scoped Beans wesentlich komfortabler, da wir uns über viele technischen Details, wie bspw. Cross-Scope-Injection, keine Gedanken machen müssen.

2.3 CDI Standard-Scopes

Da das ursprüngliche Ziel von CDI die Verbindung zwischen JSF und EJBs war, wurden in der CDI-Spezifikation sämtliche JSF-Scopes als CDI Normal-Scopes nochmals definiert. Somit stehen mit CDI standardmäßig @ApplicationScoped , @SessionScoped und @RequestScoped zur Verfügung. Zusätzlich wurde ein neuer Conversation-Scope eingeführt. In diesem Kapitel beschränken wir uns auf eine Zusammenfassung der Standard-Scopes. Konkrete Beispiele zu Web-Scopes werden wir in den Kapiteln CDI und Java EE (JSF) und Portable CDI-Erweiterungen (DS Test-Control) analysieren.

2.3.0.1 @ApplicationScoped

Wie der Name bereits vermuten lässt, existieren application-scoped Contextual-Instances nach dem ersten Zugriff bis zum Ende der Applikation. Somit handelt es sich um einen applikationsweiten Singleton.
Tipp: Der Begriff "Applikation" wird nicht genau definiert. Selbst nach über 100 Kommentaren beim Spezifikationsticket CDI-129 zeichnet sich keine vollständig akzeptierte Definition ab. Zu diesem Zeitpunkt ist lediglich klar, dass Weld und OpenWebBeans den Applikationsbegriff anders definieren, sobald die Applikation in einer EAR-Datei verpackt wird. OpenWebBeans hält sich an das referenzierte Verhalten der Servlet-Spezifikation, wohingegen sich Weld an der EJB-Welt orientiert. Daher ist mit Weld eine application-scoped Contextual-Instance in allen Web-Applikationen eines EARs gültig, sofern darauf zugegriffen werden kann.

 

In unserer Applikation können wir, wie in Listing Verwendung von Scopes , bspw. IdeaManager mit @ApplicationScoped annotieren. Der Rest der Applikation bleibt unverändert. Die Veränderung zur Laufzeit ist in diesem Fall nur im Debugger ersichtlich. Da diese Klasse zustandslos implementiert ist, gibt es keine unmittelbar merkbare Auswirkung in der Applikation und die Unit-Tests müssen nicht angepasst werden. Im Debugger können Sie jedoch feststellen, dass die Referenzvariablen, welche vom CDI-Container mit einer Contextual-Reference auf IdeaManager befüllt werden, zur Laufzeit nicht wie bisher direkt auf die Contextual-Instance verweisen, sondern auf einen entsprechenden Proxy. Wie zu erwarten war, wird außerdem nur noch eine Contexual-Instance (je Container-Start) erzeugt.
@ApplicationScoped
public class IdeaManager {
  private IdeaValidator ideaValidator;

  protected IdeaManager() {
    //needed by proxy-libs
  }

  @Inject
  protected IdeaManager(IdeaValidator ideaValidator) {
    this.ideaValidator = ideaValidator;
  }
  //...
}
Tipp: In Listing Verwendung von Scopes wird Constructor-Injection verwendet. Da die Contextual-Reference für normal-scoped Beans zur Laufzeit ein Proxy ist, muss es möglich sein für diese Klasse eine Proxy-Instanz zu erzeugen. Dies ist in Java nur mit dem Default-Konstruktor machbar. Der parametrisierte Konstruktor wird erst später für die Erzeugung der Contextual-Instance verwendet. Wie oft der Default-Konstruktor aufgerufen werden darf ist nicht spezifiziert. Initialisierungslogik sollte daher in eine mit @PostConstruct annotierte Methode verschoben werden. Dies bietet zusätzlich den Vorteil, dass sämtliche Injection-Points bereits befüllt und somit verwendbar sind.

2.3.0.2 @SessionScoped

In Hinsicht auf die Lebensdauer ist der CDI Session-Scope äquivalent zur Gültigkeit der aktuellen HTTP-Session. Folglich ist eine session-scoped Contextual-Instance ein sog. Session-Singleton. Über mehrere User-Sessions hinweg kann es mehrere Contextual-Instances eines Managed-Beans geben. Innerhalb einer User-Session ist per Definition nur eine Contextual-Instance je Managed-Bean möglich.

 

Um Mechanismen wie bspw. Session-Replication zu ermöglichen, ist dieser Scope als passivierbar definiert. In Scope-Annotationen wie bspw. javax.enterprise.context.SessionScoped wird dies durch @NormalScope(passivating = true) angegeben. Daher müssen die Bean-Klassen (in/)direkt das Interface java.io.Serializable implementieren. Referenzierte CDI-Beans müssen selbst nicht serialisierbar sein, wenn es normal-scoped Beans sind. Zur Laufzeit sorgt der CDI-Container dafür, dass die Proxys serialisierbar sind. Denn nur diese Proxys (Contextual-References), und nicht die referenzierten Contextual-Instances selbst, werden mit einem passivierbaren Bean gespeichert. CDI-Proxys arbeiten unabhängig von der Passivierung immer gleich. Mit einer eindeutigen (internen) Bean-ID wird die aktuell gültige Instanz gesucht bzw. bei Bedarf erzeugt und anschließend wird die gewünschte Methode von der Contextual-Instance aufgerufen.
Tipp: Damit es zu keinen unerwarteten Problemen zur Laufzeit kommt, schreibt CDI die Implementierung von java.io.Serializable bei Beans mit einem passivierbaren Scope vor. Diese Regel gilt immer, selbst wenn es effektiv nie in einer Applikation zu einer Passivierung von Beans kommt. Somit muss diese Regel immer beim Applikationsstart vom CDI-Container überprüft werden. In einem Fehlerfall wird der Startprozess abgebrochen und eine entsprechende Fehlermeldung ausgegeben.

2.3.0.3 @RequestScoped

Äquivalent zu den bisher beschriebenen Scope-Definitionen sind request-scoped Beans Singletons je (HTTP-)Request und somit nur für den aktuellen Request gültig. Request-Scoped Beans sind nicht passivierbar, da ein HTTP-Request selbst nicht auf einem externen Speichermedium abgelegt und zu einem späteren Zeitpunkt reaktiviert werden kann.

2.3.0.4 @ConversationScoped

Der Conversation-Scope wurde primär für JSF-basierte Applikationen spezifiziert. Er ist mit einer Session je Browser-Fenster/Tab vergleichbar (inklusive Passivierbarkeit). Allerdings muss die aktuelle Conversation manuell gestartet und beendet werden. Wird eine Conversation nicht gestartet, dann handelt es sich um eine transiente Conversation, welche am Ende des Requests vom CDI-Container zerstört werden muss. Um dies zu vermeiden kann javax.enterprise.context.Conversation injiziert und die Methode begin aufgerufen werden. Dadurch stehen sämtliche conversation-scoped CDI-Beans so lange zur Verfügung, bis explizit end auf die injizierte Conversation aufgerufen wird. Hier kommt die spezielle Integration mit JSF zu tragen. Eine Conversation wird nämlich nicht sofort beendet, sondern erst nach dem nächsten Rendering-Prozess. Durch diese Regel ist das Ende der Conversation unabhängig von der gewählten Art der JSF Navigation (Forward vs. Redirect).
Tipp: Leider sind CDI Standard-Conversations sehr stark limitiert und für viele Anwendungsfäll fachlich und technisch nicht geeignet. Aus diesem Grund empfehlen wir stattdessen die Verwendung von (Grouped-)Conversations, welche mit CODI eingeführt und in DeltaSpike übernommen wurden (siehe Kapitel Portable CDI-Erweiterungen ). Hier wurden gezielt die Einschränkungen und Schwachstellen der CDI Standard-Conversations behoben. Auf weitere Details zu (Grouped-)Conversations werden wir im Kapitel Portable CDI-Erweiterungen eingehen.

2.3.0.5 @Dependent

@Dependent ist derzeit der einzige Pseudo-Scope in der CDI Spezifikation und zugleich der Default-Scope für CDI-Beans. In IdeaFork wird bspw. für IdeaValidator kein Scope explizit angegeben. Intern behandelt der CDI-Container solche Beans als wären sie explizit mit @Dependent annotiert. In unserer Applikation wird IdeaValidator in IdeaManager injiziert. Die in IdeaManager injizierte Instanz existiert daher so lange wie die Contextual-Instance von IdeaManager . Im konkreten Fall existiert die Instanz von IdeaManager bis zum Stopp der Applikation. Im Zuge der Zerstörung der IdeaManager -Instanz wird die injizierte IdeaValidator -Instanz ebenfalls vom CDI-Container zerstört, da IdeaValidator implizit dependent-scoped ist.

 

Daraus folgt, dass die Lebensdauer einer dependent-scoped Contextual-Instance an die Lebensdauer der Contextual-Instance, in welche sie injiziert wird, gebunden ist. Somit entspricht die Lebensdauer von dependent-scoped Beans normalen Objekten, welche mit dem Schlüsselwort new erzeugt und einer Instanzvariablen zugewiesen werden. Bei dependent-scoped Beans übernimmt allerdings der CDI-Container die Erzeugung (inkl. eventueller Interceptoren,...) und bei direkter Injizierung auch die Zerstörung der dependent-scoped Contextual-Instance.
Tipp: Bei der Verwendung der CDI Scope-Annotationen @ApplicationScoped , @SessionScoped und @RequestScoped ist es wichtig auf die Pakete zu achten. Importieren Sie irrtümlich javax.faces.bean satt javax.enterprise.context , so verwenden Sie aus Sicht des CDI-Containers keine gültige Scope-Annotation und folglich wird der Dependent -Scope verwendet. Portable Erweiterungen wie bspw. DeltaSpike übersetzen JSF-Annotationen für den CDI-Container, wodurch diese häufige Fehlerquelle auf einfache Art und Weise beseitigt wird.

2.3.0.6 Manuelle Scope-Steuerung

In einer Java SE Applikation sind Web-Scopes nur beschränkt sinnvoll. Dennoch kann es erforderlich sein eine portable CDI Erweiterung zu verwenden, welche einen oder mehrere dieser Scopes verwendet, um bspw. Werte für den aktuellen Request zu cachen. Verwenden Sie CDI in einer Java SE Applikation, so können Sie trotzdem Beans mit Web-Scopes verwenden. Allerdings müssen die Scopes explizit gestartet und wieder gestoppt werden. In Web-Applikationen wird dies autom. durch den Container durchgeführt, sofern es einen aktuellen HTTP-Request bzw. eine aktive HTTP-Session gibt. Implementieren Sie bspw. einen Batch-Job, welcher als Bestandteil einer Web-Applikation deployed wird, so müssen Sie die Scope-Steuerung manuell übernehmen. Somit kann der Batch-Job im Hintergrund bspw. ohne realem HTTP-Request ausgeführt werden und auf Beans mit Web-Scopes zugreifen. Auf technische Details zur manuellen Steuerung von Standard-Scopes werden wir im Kapitel Portable CDI-Erweiterungen eingehen.
Tipp: Ein Scope ist mit einer Context -Instanz verbunden, welche sämtliche Contextual-Instances des Scopes verwaltet und entscheidet ob der Scope zu einem bestimmten Zeitpunkt aktiv ist. Da es je Applikation (in einer JVM) nur eine Context -Instanz je Scope gibt, ist diese dafür verantwortlich für den aktuellen Thread die korrekte Contextual-Instance des jeweiligen Beans zur Verfügung zu stellen. Im Umkehrschluss bedeutet dies, dass in einem Thread für ein Bean nur eine Contextual-Instance verfügbar ist. Eine einfache Analogie hierzu ist eine Map. Für jeden Key (Managed-Bean) kann ein Wert (Contextual-Instance) abgelegt werden. Abhängig von der Definition des Scopes kann es eine oder mehrere solcher Maps geben. Im Falle des Application-Scopes gibt es bspw. eine Map für die gesamte Applikation. Hingegen gibt es bspw. beim Request-Scope eine Map je (HTTP-)Request. Ist der Kontext nicht aktiv, so wird eine ContextNotActiveException geworfen, wenn auf eine Contextual-Instance in diesem Kontext zugegriffen wird.

2.4 Qualifiers

Bisher hatten wir simple Konstellationen, bei welchen wir genau ein Bean für eine Aufgabe hatten. In solchen Fällen ist die direkte typsichere Injizierung kein Problem, da beim Injection-Point der gewünschte Typ angegeben wird. In realen Applikationen treffen wir in der Regel auf umfangreichere Anforderungen. Als konkretes Beispiel führen wir ein ObjectConverter -Interface in IdeaFork ein. Implementierungen des Interfaces können Objekte in ein externes Text-Format konvertieren und umgekehrt. Um bspw. verschiedene Exportformate zu unterstützen, sind unterschiedliche Implementierungen des Interfaces erforderlich. In der Applikation soll jedoch nur gegen das Interface implementiert werden. Folglich brauchen wir einen Mechanismus, um die konkreten Implementierungen beim Injection-Point zu identifizieren, ohne die Implementierungsklasse selbst anzugeben. In CDI ist dies durch Qualifier-Annotationen abgedeckt.

 

In IdeaFork wollen wir XML und JSON als externe Formate unterstützen. Für XML verwenden wir vorerst JAXB und für JSON eine Bibliothek namens Gson (http://code.google.com/p/google-gson/). Wir beginnen mit einfachen "Marker-Qualifiern". Ähnlich wie Marker-Interfaces dienen sie als zusätzliche Markierung. Für die Erstellung der Qualifier-Annotationen @XML und @JSON müssen wir uns überlegen an welchen Stellen diese Annotationen verwendet werden sollen. In einem ersten Schritt wollen wir die Implementierungsklassen annotieren ( ElementType.TYPE ). Später werden wir Producer-Methoden kennenlernen, für welche ElementType.METHOD erforderlich wäre. Bei möglichen Injection-Points können wir Felder ( ElementType.FIELD ) sowie Methoden-(/Konstruktor-)Parameter ( ElementType.PARAMETER ) unterstützen. Damit der CDI-Container selbst erstellte Annotationen als Qualifier erkennt, müssen wir unsere Annotationen mit javax.inject.Qualifier annotieren. Listing Einfache Qualifier-Annotation als Marker zeigt die vollständige Implementierung von @JSON . @XML unterscheidet sich nur durch den Namen der Annotation.
@Target({TYPE, FIELD})
@Retention(RUNTIME)
@Qualifier
public @interface JSON {}
Mit den Qualifier-Annotationen @JSON und @XML können wir beide Implementierungen identifizieren. Wie Listing Bean-Klasse mit Qualifier zeigt, annotieren wir hierfür die Implementierungsklassen mit dem entsprechenden Qualifier.
@JSON
@ApplicationScoped
public class JSONConverter implements ObjectConverter {
  @Override
  public <T> T toObject(String value, Class<T> type) {
    return new Gson().fromJson(value, type);
  }

  @Override
  public String toString(Object entity) {
    return new Gson().toJson(entity);
  }
}
Da die Implementierung in Listing Bean-Klasse mit Qualifier zustandslos ist, könnten wir ebenso auf den Dependent -Scope zurückgreifen, wodurch nicht mehr benötigte Instanzen laufend durch den Garbage-Collector entsorgt werden müssen. Der Proxy-Overhead ist mittlerweile minimal und daher gibt es hier keine "richtige" und keine "falsche" Wahl.

 

Um bspw. die Exportlogik unseres JSON-Konverters zu testen, können wir diesen in einem Unit-Test verwenden. In Listing Injection-Point mit Qualifier injizieren wir den Konverter mit unserem allgemeinen ObjectConverter -Interface in Verbindung mit @JSON als Qualifier. Ohne Qualifier-Annotationen könnten wir nie ein Interface mit mehreren Implementierungen injizieren und müssten direkt die Implementierungsklasse beim Injection-Point angeben.

 

Wird kein Qualifier beim Injection-Point angegeben, so verwendet der CDI-Container javax.enterprise.inject.Default als Standard-Qualifier. Geben wir in unserem Beispiel @JSON nicht an, dann kann kein passendes CDI-Bean gefunden werden, da wir bisher keine Implementierung explizit oder implizit mit dem Default -Qualifier annotiert haben. In solchen Fällen wird der Containerstart mit einer UnsatisfiedResolutionException abgebrochen.

 

Würden wir komplett auf Qualifier verzichten und trotzdem den Interface-Typ beim Injection-Point verwenden, dann wäre eine AmbiguousResolutionException der Grund für den Abbruch des Containerstarts, da mehrere Implementierungen mit dem impliziten Default -Qualifier verfügbar wären.
@RunWith(CdiTestRunner.class)
public class QualifierTest {
  private String topic = "Learn CDI-Qualifiers";
  private String category = "Education";

  @Inject
  @JSON
  private ObjectConverter objectConverterJSON;

  @Inject
  private IdeaManager ideaManager;

  @Test
  public void jsonConversion() {
    Idea exportedIdea = ideaManager.createIdeaFor(topic, category);

    String jsonString =
      objectConverterJSON.toString(exportedIdea);

    Idea importedIdea =
      objectConverterJSON.toObject(jsonString, Idea.class);

    Assert.assertTrue(exportedIdea.equals(importedIdea));
  }
}
In großen Projekten würden wir nach kurzer Zeit eine Vielzahl an Qualifier-Annotationen bekommen, wenn wir zu jeder Implementierung eine Annotation benötigen würden. Aus diesem Grund ist es möglich beliebige Zusatzinformationen in einer Qualifier-Annotation aufzunehmen. In unserem Fall können wir statt @XML und @JSON einen Qualifier mit dem Namen @ExternalFormat verwenden. Um weiterhin zwischen XML und JSON unterscheiden zu können, fügen wir ein enum hinzu. Listing Qualifier mit Annotation-Attribut zeigt die Implementierung von @ExternalFormat und Listing Verwendung von Qualifiern mit Annotation-Attribut die geänderte Verwendung bei der Implementierung und einem Injection-Point.
@Target({TYPE, METHOD, FIELD, PARAMETER})
@Retention(RUNTIME)
@Qualifier
public @interface ExternalFormat {
  TargetFormat value();

  enum TargetFormat {
    XML, JSON
  }
}
@ExternalFormat(ExternalFormat.TargetFormat.JSON)
@ApplicationScoped
public class JSONConverter implements ObjectConverter {
    //...
}

@RunWith(CdiTestRunner.class)
public class QualifierTest {

  @Inject
  @ExternalFormat(JSON)
  private ObjectConverter objectConverterJSON;

  //...
}
Bei verschiedenen Anwendungsfällen kann es erforderlich sein Informationen zur Verfügung zu stellen, welche von CDI ignoriert werden sollen. Für solche Fälle können Annotation-Attribute mit @Nonbinding annotiert werden. In Listing Exkludiertes Annotation-Attribut wird @ExternalFormat um eine optionale Beschreibung erweitert, welche vom CDI-Container nicht für die Qualifier-Logik verwendet wird.
@Target({TYPE, FIELD})
@Retention(RUNTIME)
@Qualifier
public @interface ExternalFormat {
  TargetFormat value();

  @Nonbinding
  String description() default "";

  enum TargetFormat {
    XML, JSON
  }
}

2.5 Dynamische Verwendung von CDI-Beans

Bisher haben wir "direkte" Injizierung verwendet. Es gab immer einen expliziten Injection-Point, welcher vom CDI-Container zur Laufzeit mit einer Contextual-Reference automatisch befüllt wird. Diese Injection-Points werden während dem Containerstart auf ihre Gültigkeit überprüft, da es für solche Injection-Points jeweils genau ein Managed-Bean geben muss. Es gibt jedoch Fälle in denen wir optionale oder sogar mehrere Managed-Beans benötigen. Für solche Anwendungsfälle müssen Beans dynamisch gesucht werden.

2.5.0.1 Indirektion mit Instance

Im vorherigen Kapitel haben wir den ObjectConverter in Kombination mit dem Qualifier für JSON bzw. XML direkt injiziert. Listing Dynamische Injizierung mit Instance zeigt das direkte Äquivalent über dynamische Injection mit Hilfe von javax.enterprise.inject.Instance . Dieses Interface erweitert javax.inject.Provider , welches die Methode get definiert. Erst durch den Aufruf dieser Methode wird der Lookup durchgeführt.
@RunWith(CdiTestRunner.class)
public class LookupTest {
  //...

  @Inject
  @ExternalFormat(JSON)
  private Instance<ObjectConverter> objectConverterJSONInstance;

  @Test
  public void jsonConversion() {
    //...
    String jsonString =
      objectConverterJSONInstance.get().toString(exportedIdea);
    //...
  }
}
Vor dem Aufruf von get kann mithilfe der Methode isUnsatisfied überprüft werden, ob der Lookup überhaupt ein Ergebnis liefern würde oder zu einer UnsatisfiedResolutionException führt. Über diesen Mechanismus sind optionale Beans möglich. So können wir bspw. unseren ExternalFormat -Qualifier um ein zusätzliches Format (CSV) erweitern. Wir stellen jedoch keine Implementierung zur Verfügung und erhalten dadurch ein optionales Bean. Ein Plug-in für die Applikation könnte nachträglich eine solche Implementierung hinzufügen. Oftmals wird dieses Konzept für simple Interfaces (ohne Qualifier-Annotationen) mit nur einer optionalen Implementierung verwendet. Listing Dynamische Injizierung für optionale Beans zeigt einen entsprechenden Unit-Test.
@RunWith(CdiTestRunner.class)
public class LookupTest {
  @Inject
  @ExternalFormat(CSV)
  private Instance<ObjectConverter> objectConverterCSVInstance;

  @Test(expected = UnsatisfiedResolutionException.class)
  public void optionalConverter() {
    Assert.assertTrue(objectConverterCSVInstance.isUnsatisfied());
    objectConverterCSVInstance.get();
  }
}
Eine weitere Möglichkeit ist die Verwendung mehrerer Beans vom gleichen Typ. Vor dem Aufruf von get kann mit isAmbiguous überprüft werden, ob das Ergebnis des Methodenaufrufs zu mehreren Beans und somit zu einer AmbiguousResolutionException führen würde. Hierfür können wir bspw. einen Lookup auf alle Implementierungen vom Type ObjectConverter machen. CDI stellt für einen solchen Lookup einen "virtuellen" Qualifier namens javax.enterprise.inject.Any zur Verfügung. Listing Dynamische Injizierung mit mehreren Beans zeigt einen entsprechenden Unit-Test.
@RunWith(CdiTestRunner.class)
public class LookupTest {
  @Inject
  @Any
  private Instance<ObjectConverter> converterInstance;

  @Test(expected = AmbiguousResolutionException.class)
  public void ambiguousConverter() {
    Assert.assertTrue(converterInstance.isAmbiguous());
    converterInstance.get();
  }
}
Es gibt jedoch auch Anwendungsfälle, bei denen mehrere Beans vom gleichen Typ benötigt werden. Ein Beispiel hierfür sind Plug-ins. In solchen Fällen kann nicht die Methode get verwendet werden. Allerdings erweitert Instance ebenfalls das Interface Iterable und daher kann die injizierte Instance bspw. in einer Schleife verwendet werden. Listing Dynamische Injizierung mit mehreren Beans illustriert dies anhand eines autom. Tests aller ObjectConverter -Beans.
@RunWith(CdiTestRunner.class)
public class LookupTest {
  @Inject
  @Any
  private Instance<ObjectConverter> converterInstance;

  @Inject
  private IdeaManager ideaManager;

  @Test
  public void allConverters() {
    Assert.assertTrue(converterInstance.isAmbiguous());
    Assert.assertFalse(converterInstance.isUnsatisfied());

    for (ObjectConverter converter : converterInstance) {
      Idea idea = ideaManager.createIdeaFor(...);
      String exported = converter.toString(idea);
      Assert.assertTrue(
        converter.toObject(exported, Idea.class).equals(idea));
    }
  }
}

2.5.0.2 Beans via BeanManager finden

Der BeanManager dient zur Interaktion mit dem CDI-Container. Über ihn können Sie bspw. überprüfen ob eine Annotation ein Qualifier ist, Events feuern, Beans manuell suchen und vieles mehr. Eine Referenz auf den BeanManager kann injiziert oder bspw. via JNDI abgerufen werden. Normalerweise ist es nicht erforderlich Contextual-References mit dem BeanManager zu holen. In vielen Fällen wird ein solcher manueller Lookup in Klassen verwendet, die nicht durch den CDI-Container verwaltet werden. Ohne CdiTestRunner wäre dies eine der Möglichkeiten Unit-Tests ohne spezielle CDI-Unterstützung zu implementieren. Listing Manueller Lookup via BeanManager zeigt die Schritte, um eine Contextual-Reference auf IdeaManager zu bekommen.
Set<Bean<?>> beans = this.beanManager.getBeans(IdeaManager.class);
Bean<?> bean = beanManager.resolve(beans);
CreationalContext<?> creationalContext =
  beanManager.createCreationalContext(bean);
this.ideaManager = (IdeaManager)this.beanManager.getReference(
  bean, IdeaManager.class, creationalContext);
Im ersten Schritt ist das Ergebnis ein Set von Beans, da hier bspw. alternative Beans, welche wir noch kennenlernen werden, enthalten sein können. Erst durch den Aufruf von resolve im zweiten Schritt wird ein gültiges Ergebnis geliefert. Selbst wenn in vielen Fällen das Set nur einen Eintrag enthält, dürfen Sie den zweiten Schritt nicht auslassen und immer das erste Bean im Set verwenden, da dies später zu unerwarteten Seiteneffekten führen kann. Auf Basis der Managed-Bean-Definition kann der sog. CreationalContext erzeugt werden. Zusammen mit dem Bean und dem erwateten Zieltyp muss dieser an getReference übergeben werden, um schließlich eine Contextual-Reference zu erhalten.

2.5.0.3 Dependent-Scoped Beans

Auch bei dynamischer Injizierung bzw. einem manuellem Lookup nehmen dependent-scoped Beans eine Sonderstellung ein. Denn das Bean wird für jeden dieser dynamischen Aufrufe vom CDI-Container erzeugt und nach außen gegeben. Daraus folgt, dass der Container die Contextual-Instance danach nicht mehr verwaltet und somit auch nicht mehr für die Zerstörung dieser zuständig ist. Selbst wenn in einem weiteren Schritt eine manuelle Zuweisung an eine Instanzvariable eines normal-scoped Beans durchgeführt wird, handelt es sich weiterhin um eine manuell verwaltete Instanz, da diese Instanzvariable kein regulärer Injection-Point ist. In solchen speziellen Fällen muss die Zerstörung manuell durchgeführt werden. Hierfür müssen Sie sich die CreationalContext -Instanz und die Managed-Bean-Definition speichern, um später via bean.destroy(contextualInstance, creationalContext) das dependent-scoped Bean korrekt zerstören zu können. Manuelle Bean-Lookups sollten daher immer die Ausnahme bleiben und mit Sorgfalt umgesetzt werden.

2.5.0.4 Literals

Annotationen sind in Java statische Metadaten, welche nicht manuell instanziiert werden können. Im vorherigen Kapitel haben wir gesehen, dass es Qualifier-Annotationen mit Attributen gibt. In Kombination mit direkter Injizierung stoßen wir auf keine Einschränkungen. Für das dynamische Auffinden eines Beans mit Qualifier benötigen wir allerdings Instanzen einer Annotation, damit wir die gewünschten Werte zur Verfügung stellen können. Um, trotz der Einschränkung von Java, Instanzen von Annotationen zu erzeugen, bedient sich CDI einem Trick. Durch die Erstellung einer Annotation-Literal -Klasse kann der JVM eine manuell erstellte Instanz der dazugehörigen Annotation zur Verfügung gestellt werden. Listing Annotation Literal für ExternalFormat zeigt ein Literal für ExternalFormat . Hierfür muss von javax.enterprise.util.AnnotationLiteral abgeleitet und der Annotation-Typ implementiert werden. Wie bei einem Interface müssen sämtliche Annotation-Methoden implementiert werden. Üblicherweise ändern sich Werte von Literals nicht und somit können die gewünschten Werte direkt über einen parametrisierten Konstruktor übergeben werden.
public class ExternalFormatLiteral
  extends AnnotationLiteral<ExternalFormat>
  implements ExternalFormat {
    private final TargetFormat value;

    public ExternalFormatLiteral(TargetFormat value) {
      this.value = value;
    }

    @Override
    public TargetFormat value() {
      return this.value;
    }

    @Override
    public String description() {
      return "";
    }
}
Ausgerüstet mit der Literal-Klasse können wir die select -Methode von Instance verwenden, um die Suche mit einem Qualifier einzuschränken. Beim Injection-Point von Instance wird in solchen Fällen normalerweise @Any verwendet, da der effektive Qualifier später dynamisch definiert wird. Listing Verwendung von Annotation Literal mit Instance illustriert die Verwendung unseres Annotation-Literals in Kombination mit Instance .
@RunWith(CdiTestRunner.class)
public class LookupTest {
  @Inject
  @Any
  private Instance<ObjectConverter> converterInstance;

  @Inject
  private IdeaManager ideaManager;

  @Test
  public void xmlConversion() {
    Idea exportedIdea = ideaManager.createIdeaFor(...);

    Assert.assertTrue(converterInstance.isAmbiguous());
    Assert.assertFalse(converterInstance.isUnsatisfied());

    String xmlString = converterInstance.select(
      new ExternalFormatLiteral(XML)).get()
      .toString(exportedIdea);

    //...
  }
}
Unser Annotation-Literal können wir natürlich ebenso für den Lookup via BeanManager verwenden. Listing Verwendung von Annotation Literal mit BeanManager erweitert Listing Verwendung von Annotation Literal mit Instance um einen zweiten Bean-Lookup via BeanManager statt via Instance .
@RunWith(CdiTestRunner.class)
public class LookupTest {
  @Inject
  private BeanManager beanManager;

  @Inject
  @Any
  private Instance<ObjectConverter> converterInstance;

  @Inject
  private IdeaManager ideaManager;

  @Test
  public void xmlConversion() {
    Idea exportedIdea = ideaManager.createIdeaFor(...);

    Assert.assertTrue(converterInstance.isAmbiguous());
    Assert.assertFalse(converterInstance.isUnsatisfied());

    String xmlString = converterInstance.select(
      new ExternalFormatLiteral(XML)).get()
      .toString(exportedIdea);

    Set<Bean<?>> beans = beanManager.getBeans(
      ObjectConverter.class, new ExternalFormatLiteral(XML));
    Bean<?> bean = beanManager.resolve(beans);
    CreationalContext<?> creationalContext =
      beanManager.createCreationalContext(bean);

    ObjectConverter xmlConverter =
      (ObjectConverter)this.beanManager.getReference(
        bean, ObjectConverter.class, creationalContext);

    Idea importedIdea =
      xmlConverter.toObject(xmlString, Idea.class);
    Assert.assertTrue(exportedIdea.equals(importedIdea));
  }
}

2.5.0.5 Type-Literal

Einen vergleichbaren Helper gibt es für parametrisierte Klassen und wird Type-Literal genannt. Die Anwendungsfälle hierfür sind jedoch eher rar. Sollten Sie auf einen solchen stoßen, dann können Sie TypeLiteral ähnlich zu Annotation-Literals verwenden. Ein Beispiel hierfür ist: new TypeLiteral<MyBean<MyType>>(){} .

2.6 Producer-/Disposer- Methoden

In den meisten Fällen ist es ausreichend eine einfache Java-Klasse als CDI-Bean zu verwenden. Der CDI-Container instanziiert die Klasse und führt verschiedene Zusatzfunktionalitäten wie bspw. Dependency-Injection aus. Spezielle Initialisierungslogik kann in einer mit @javax.annotation.PostConstruct annotierten Methode implementiert werden und vor der Zerstörung der Instanz kann mithilfe einer @javax.annotation.PreDestroy annotierten Methode dafür gesorgt werden, dass die erforderlichen Aufräumarbeiten durchgeführt werden.

 

Es gibt allerdings Anwendungsfälle, in denen komplexere Anforderungen gegeben sind oder Klassen verwendet werden müssen, für die spezielle Initialisierungslogik bzw. Aufräumarbeiten erfolgen müssen. Einer dieser Anwendungsfälle ist die typsichere Konfiguration von Applikationen. Viele Applikationen sind über konfigurierte Werte anpassbar. Diese Werte werden entweder in Konfigurationsdateien mitgeliefert oder aus einer zentralen Quelle wie bspw. einer Datenbank geladen. Hierbei können bspw. Tippfehler zu Abbrüchen bzw. schwerwiegenden Fehlern in der Applikation führen. In manchen Fällen werden solche Fehler erst spät entdeckt, vor allem wenn konfigurierte Werte erst bei Bedarf eingelesen und konvertiert werden. Um derartigen Fehlern entgegenzuwirken erstellen wir im nächsten Schritt eine typsichere Applikationskonfiguration und eine Producer-Methode für die manuelle Initialisierung des Konfigurationsobjekts.
Tipp: Typsichere Konfigurationen können einfache POJOs sein, welche als CDI-Beans verwendbar sind. CDI-Erweiterungen wie bspw. CODI und DeltaSpike stellen über dieses simple Konzept typsichere Konfigurationen zur Verfügung. Die Default-Werte der Konfiguration sind oftmals hartcodiert. Durch alternative Implementierungen, welche wir im Kapitel [ Beans ersetzen] näher betrachten werden, können die vorgegebenen Werte auf einfache Weise angepasst werden.
In IdeaFork wollen wir vorerst eine simple Konfigurationsdatei anbinden und deren Werte typsicher in der Applikation zur Verfügung stellen. Die konfigurierten Werte hinterlegen wir in einer Property-Datei als Key/Value-Paare. Einlesen können wir sie mit Hilfe von java.util.ResourceBundle . In unserer Konfigurationsklasse namens ApplicationConfig übergeben wir im Konstruktor nur eine ResourceBundle -Instanz und laden Informationen wie bspw. den Applikationsnamen. Die Logik für die Erzeugung der ResourceBundle -Instanz wollen wir in einen Producer auslagern. Um die Typsicherheit besser zu veranschaulichen fügen wir zusätzlich einen eigenen ApplicationVersion -Typ hinzu. In Listing Producer-Methode erzeugt die Producer-Methode ein application-scoped Bean vom Typ ApplicationConfig . Diese Implementierung ist sehr einfach gehalten. Zusätzlich könnte bspw. Bean-Validation verwendet werden, um die geladenen und konvertierten Werte auch zu validieren. Darüber hinaus unterstützen auch Producer optional Qualifier-Annotationen. Diesen Vorteil werden wir etwas später für die dynamische Selektierung einer Default-Implementierung nutzen.
welche in einfachen Anwendungsfällen wie diesem nicht erforderlich sind.
Tipp: Würde nur die Annotation @Produces verwendet, so würden wir einen Producer für ein implizit dependent-scoped Bean mit Default -Qualifier erstellen.

 

Ohne die Verwendung eines Qualifiers stehen wir jedoch wieder vor dem Problem, dass wir zwei Managed-Beans mit dem gleichen Typ haben. Da wir das Ergebnis der Producer-Methode als Managed-Bean für ApplicationConfig verwenden wollen und nicht die ApplicationConfig -Klasse selbst, müssen wir dem CDI-Container signalisieren, dass die ApplicationConfig -Klasse nicht verwendet werden soll. Wie wir später noch kennenlernen werden, ist dies über ein Veto des Beans möglich. Hierfür müssen wir entweder eine CDI-Erweiterung erstellen oder eine bestehende verwenden. Erst ab CDI 1.1 kann ein solches Veto über die @Vetoed Annotation ohne Zusätze verwendet werden. Mit CDI 1.0 gibt es dennoch einen simplen Trick ohne die zuvor erwähnten Erweiterungen. Die Annotation @Typed ermöglicht die explizite Typisierung. Implementiert bspw. eine Klasse eines CDI-Beans zwei Interfaces, so kann mit @Typed festgelegt werden, dass diese Bean-Klasse für den Injection-Prozess nur einen der beiden Typen bekommt. Diese Annotation kann auch ohne Angabe eines Typs verwendet werden, wodurch das Managed-Bean für typsichere Injizierung unsichtbar wird. Annotieren wir daher die ApplicationConfig -Klasse mit @Typed() , nehmen wir eines der beiden Beans aus dem Spiel und somit existiert nur noch die Producer-Methode mit dem Typ ApplicationConfig . Listing Klasse für eine typsichere Konfiguration zeigt einen Ausschnitt der Klasse ApplicationConfig und Listing Producer-Methode die Producer-Methode.
@Typed()
public class ApplicationConfig {
  private String applicationName;
  private ApplicationVersion applicationVersion;

  protected ApplicationConfig() {
    //needed for creating a proxy
  }

  public ApplicationConfig(ResourceBundle config) {
    applicationName = config.getString("name");
    applicationVersion =
      new ApplicationVersion(config.getString("version"));
  }
  //+ Getter-Methods

  public static class ApplicationVersion {
    //...
  }
}
@Produces
@ApplicationScoped
public ApplicationConfig exposeConfig() {
  ResourceBundle config = ResourceBundle.getBundle("app-config");
  return new ApplicationConfig(config);
}
Wie wir bereits wissen wird für normal-scoped Beans ein Proxy erzeugt. Da die Producer-Methode in Listing Producer-Methode ein normal-scoped Bean definiert, muss es folglich in der ApplicationConfig -Klasse einen Default-Konstruktor geben.
Tipp: Um Klassen außerhalb von CDI-Archiven als CDI-Beans zu verwenden, gibt es neben der Verwendung von Producer-Methoden (oder -Feldern) verschiedene Möglichkeiten, welche im Kapitel Portable CDI-Erweiterungen vorgestellt werden. Komplexere Initialisierungslogik kann hingegen nur mit einer Producer-Methode umgesetzt werden.

2.6.0.1 Manuelle Injection

CDI-Beans, welche über einen Producer erzeugt werden, haben ein paar Einschränkungen. Der CDI-Container führt keine Dependency-Injection auf das erzeugte Objekt aus und es werden auch keine Interceptoren hinzugefügt, da die Erzeugung und Initialisierung der Contextual-Instance manuell im Producer vorgenommen wird und Interceptoren die Producer-Methode intercepten und nicht auf das Ergebnis des Producers angewandt werden. Mit dem Trick aus Listing Manueller Field- und Method-Injection Trick können Sie Field- und Method-Injection manuell anstoßen. In dem Quelltextausschnitt steht unmanagedBean für die manuell erzeugte Instanz. Beachten Sie jedoch, dass Sie dependent-scoped Beans zwar injecten können, aber durch den fehlenden CreationalContext der injizierten Beans diese nicht korrekt zerstören können. Allerdings ist die primäre Auswirkung, dass dependent-scoped Beans in Kombination mit einer solchen manuellen Injection keine @PreDestroy annotierten Methoden verwenden können. Mit dem entsprechenden Detailwissen über diese Beans können bei Bedarf die Callback-Methoden manuell in der (optionalen) Disposer-Methode aufgerufen werden.
CreationalContext creationalContext =
  this.beanManager.createCreationalContext(null);
AnnotatedType annotatedType = this.beanManager
  .createAnnotatedType(unmanagedBean.getClass());
InjectionTarget injectionTarget = this.beanManager
  .createInjectionTarget(annotatedType);
injectionTarget.inject(unmanagedBean, creationalContext);

2.6.0.2 Disposer-Methoden

Das optionale Gegenstück zur Producer-Methode ist die Disposer-Methode. Wie bereits erwähnt unterstützen Beans, welche mit einem Producer erzeugt werden keine Lifecycle-Callbacks. Die Aufgabe von @PostConstruct übernimmt der Producer selbst und statt @PreDestroy kann eine sog. Disposer-Methode verwendet werden. Wie Listing Disposer-Methode zeigt handelt es sich hierbei um eine Methode mit mindestens einem Parameter. Der Typ und die Qualifier müssen mit jenen der Producer-Methode übereinstimmen. Zusätzlich muss der erste verpflichtende Parameter mit @Disposes annotiert werden. Erst durch diese Annotation erkennt der CDI-Container die Methode als Disposer. Da ApplicationConfig application-scoped ist, wird die Disposer-Methode vor jedem Applikationsstopp ausgeführt. In unserem einfachen Beispiel loggen wir die Version der Applikation, welche heruntergefahren wird.
public void onDispose(@Disposes
  ApplicationConfig applicationConfig) {
    LOGGER.info("shutting down v"
      + applicationConfig.getApplicationVersion());
}

2.6.0.3 Mit Producer-Methoden Beans selektieren

Producer-Methoden können bspw. ebenfalls zur Selektion einer Default-Implementierung verwendet werden. Beginnen wir vorerst mit einer möglichst einfachen Producer-Methode. Den Default -Qualifier haben wir bei den ObjectConverter -Implementierungen weder explizit noch implizit in Verwendung. Somit müssen wir keinen neuen Qualifier definieren, sondern können auf den Default -Qualifier zurückgreifen. In Listing Einfache Selektion via Producer ist die minimale Variante dargestellt.
public class CurrentObjectConverterProducer {
  @Produces
  protected ObjectConverter defaultConverter (
    @ExternalFormat(JSON) ObjectConverter objectConverter) {
      return objectConverter;
  }
}
Der Default -Qualifier sowie der Dependent -Scope müssen nicht explizit angegeben werden. Wir könnten natürlich die entsprechende Implementierung manuell instanziieren, statt einen Injection-Point zu verwenden. Wie bereits zuvor erwähnt würden wir dabei auf einige CDI-Funktionalitäten verzichten. Listing Einfache Selektion via Producer veranschaulicht zusätzlich, dass Producer-Methoden optional Parameter verwenden können. Jeder Parameter stellt dabei einen eigenen (impliziten) Injection-Point dar, welcher durch den CDI-Container wie gewohnt autom. injiziert wird. In diesem einfachen Producer wählen wir durch den Injection-Point die gewünschte Implementierung aus und stellen sie unverändert mit einem anderen Qualifier, in unserem Beispiel dem impliziten Default -Qualifier, zur Verfügung. Soll in der Applikation die Default-Implementierung verwendet werden, so kann diese dadurch wie erwartet injiziert werden. Eine spätere Umstellung der Default-Implementierung ist jetzt einfach möglich, da nur der Injection-Point der Producer-Methode angepasst werden muss.

 

Natürlich können wir den Producer etwas flexibler gestalten. Hierfür können wir bspw. ApplicationConfig um einen Eintrag für das Default-Format erweitern. Anschließend werten wir diesen neuen Konfigurationseintrag im zuvor erstellen Producer aus. Mit diesem Ansatz muss die Implementierung des Producers nicht umgestellt werden, wenn das Default-Format auf ein bereits unterstütztes Format geändert werden soll. Listing Erweiterte ApplicationConfig zeigt die erweiterte Implementierung von ApplicationConfig und Listing Konfigurierbare Default-Implementierung die Verwendung in der Producer-Methode.
@Typed()
public class ApplicationConfig {
  //...
  private ExternalFormat.TargetFormat defaultExternalFormat;

  protected ApplicationConfig() {
    //needed for creating a proxy
  }

  public ApplicationConfig(ResourceBundle config) {
    //...
    this.defaultExternalFormat =
      ExternalFormat.TargetFormat.valueOf(
      config.getString("defaultExternalFormat"));
  }

  public ExternalFormat.TargetFormat getDefaultExternalFormat() {
    return defaultExternalFormat;
  }
  //...
}
@ApplicationScoped
public class CurrentObjectConverterProducer {
  @Produces
  @Default
  @Dependent
  protected ObjectConverter defaultConverter(
    @ExternalFormat(XML) ObjectConverter objectConverterXml,
    @ExternalFormat(JSON) ObjectConverter objectConverterJson,
    ApplicationConfig applicationConfig) {

      switch (applicationConfig.getDefaultExternalFormat()) {
        case JSON:
          return objectConverterJson;
        default:
          return objectConverterXml;
      }
  }
}
Die Verwendung von @ApplicationScoped in Listing Konfigurierbare Default-Implementierung ist in unserem Fall optional und sorgt dafür, dass der CDI-Container nicht für jeden Aufruf der Producer-Methode eine neue CurrentObjectConverterProducer -Instanz erzeugen muss. Da der Producer zustandslos implementiert ist, ändert sich das Ergebnis im Vergleich zu Listing Einfache Selektion via Producer nicht.

 

Der Default -Qualifier ist bei Injection-Points ebenfalls optional. Daraus folgt, dass wir ihn bisher immer implizit verwendet haben. Sie müssen sich nur überlegen, ob Sie solche impliziten Verwendungen in einer Applikation zulassen wollen, wenn Sie jede konkrete Implementierung mit einem Qualifier versehen. In manchen Projekten wird versucht über explizite Qualifier die Fehlerwahrscheinlichkeit zu reduzieren und dies würde durch den zuvor vorgestellten Ansatz ausgehebelt werden. Möchten Sie die explizite Angabe eines Qualifiers erzwingen, dann können Sie statt dem Default -Qualifier einen eigenen Qualifier definieren. Beliebte Namen für solche Qualifier sind @Current und @Active .
Tipp: CDI 1.0 spezifiziert für Injection-Points den zusätzlichen Qualifier @New , welcher seit CDI 1.1 als Deprecated markiert ist. Durch diesen Qualifier injiziert der CDI-Container immer eine dependent-scoped Contextual-Instance eines Beans unabhängig vom explizit angegebenen Scope des entsprechenden Managed-Beans. Dies wurde anfangs öfters für verschiedene Tricks in Kombination mit Producer-Methoden verwendet, ist jedoch aufgrund der höheren Komplexität im Normalfall nicht empfehlenswert.

2.6.0.4 Producer-Felder

Abgesehen von Producer-Methoden können auch Producer-Felder verwendet werden. Wie wir später sehen werden, können Producer-Felder in Kombination mit Ressource-Injection in einem EE-Server ein paar Codezeilen einsparen. Äquivalent zu Producer-Methoden werden Producer-Felder mit @Produces annotiert. Für die meisten Producer sind Producer-Felder nicht empfehlenswert. Der Unterschied zwischen einer simplen Methode und einem Feld ist in der Praxis minimal. Jedoch ist das Debugging in Kombination mit Producer-Feldern bedeutend komplexer und darüber hinaus erhalten wir durch Producer-Felder eine zustandsbehaftete Producer-Klasse, wodurch der gewählte Scope dieser zu unterschiedlichen Ergebnissen führen kann. Ein solcher Effekt ist möglich, wenn bspw. bei einem application-scoped CDI-Bean eine Instanzvariable nur einmal initialisiert wird, diese jedoch als Producer für ein weiteres Bean mit einem kürzeren Scope verwendet wird. Normalerweise möchte man hier immer eine neue Instanz erhalten, welche im angegebenen Scope abgelegt werden soll. Durch den beschriebenen Effekt wird die Instanzvariable im application-scoped Bean nur einmal erzeugt und kann daher zu unerwateten Ergebnissen führen. Ob dies tatsächlich Seiteneffekte verursacht, hängt von der konkreten Konstellation ab. In Kombination mit Resource-Injection von Java EE ist dies bspw. kein Problem, da hier ebenfalls mit Proxy-Instanzen gearbeitet wird.

2.7 Events

In einer Spezifikation zum Thema Dependency-Injection und Context-Management ist die Definition eines Event-Systems für viele unerwartet. Allerdings sind die durch CDI spezifizierten Events nur durch die Kombination von einigen der bisher vorgestellten CDI-Konzepte möglich. Zusammengefasst sind CDI-Events eine entkoppelte Implementierung des Observer-Entwurfsmusters. Entsprechend werden CDI-Events derzeit nur synchron verarbeitet, wodurch es sequentiellen Methodenaufrufen gleichkommt. Der Vorteil dabei ist die Entkoppelung von Event-Erzeuger und Event-Verbraucher. Durch die überaus elegante Umsetzung ist es erfreulich, dass CDI-Events zusätzlich für den Containerlebenszyklus verwendet werden. Solche Container-Events werden wir uns im Kapitel Portable CDI-Erweiterungen näher ansehen.
Tipp: Eine asynchrone Verteilung wird derzeit nicht durch die Spezifikation unterstützt und ist somit implementierungsabhängig. So ist es bspw. für OpenWebBeans möglich ein entsprechendes Add-on zu implementieren. Unabhängig ob es sich um proprietäre asynchrone CDI-Events oder eine manuell angestoßene asynchrone Verarbeitung handelt, müssen alle statusrelevanten Informationen direkt mit der Event-Instanz übergeben werden. Denn für eine asynchrone Verarbeitung wird ein neuer Thread gestartet, welcher bspw. einen neuen Request-, Session-,... Scope bekommt. Wie dies genau funktioniert und was Sie hierfür beachten müssen, werden wir später im Detail analysieren.

 

Bevor wir in IdeaFork CDI-Events sinnvoll verwenden können, müssen wir zusätzliche Applikationslogik hinzufügen. Wir erstellen eine einfache In-Memory Repository-Implementierung, mit welcher wir Ideen speichern, laden und löschen können.

 

Im Hintergrund wird eine einfache Map verwendet, um Idea -Instanzen zu speichern. Die einzige Besonderheit ist die Simulierung von detached Entitäten, welche wir in einem späteren Schritt mit JPA autom. bekommen. Für unsere simple In-Memory Implementierung können wir diesen Effekt mit Hilfe einer einfachen Clone-Methode nachstellen, welche verwendet wird bevor eine Entität zurückgegeben oder abgelegt wird. Somit werden unerwartete Effekte vermieden, welche auftreten können wenn die Referenz auf die Entität nach dem Repository-Aufruf für weitere Änderungen verwendet wird. Als Key verwenden wir eine generierte ID, die wir vorerst in der Idea -Klasse selbst hinzufügen.

 

Mit dieser neuen Funktionalität in IdeaFork können wir ein sinnvolles CDI-Event hinzufügen. Beispielsweise können wir ein Event erzeugen, sobald eine Idea -Instanz gespeichert wurde. Um dies zu erreichen, injizieren wir javax.enterprise.event.Event in unsere soeben angelegte IdeaRepository -Klasse und typisieren das Event auf unseren eigenen Event-Typ. Als Event-Typ kann jede beliebige Klasse verwendet werden. Für einfache Anwendungsfälle kann die Klasse der Entität selbst verwendet werden. Listing CDI-Events feuern illustriert eine solche einfache Verwendung. Später werden wir eine eigene Event-Klasse einführen, da dies in realen Applikationen aussagekräftiger ist und zusätzliche Informationen mitgegeben werden können. Im zweiten Teil von Listing CDI-Events feuern wird die injizierte Event-Instanz verwendet, um ein Event zu feuern.
public class IdeaRepository {
  @Inject
  private Event<Idea> ideaSavedEvent;

  private Map<String, Idea> entityMap =
    new ConcurrentHashMap<String, Idea>();

  public void save(Idea entity) {
    entityMap.put(entity.getId(), clone(entity));
    ideaSavedEvent.fire(entity);
  }
  //...
}
Auch CDI-Qualifier sind in Kombination mit CDI-Events möglich. Sie können beim Injection-Point des Events angegeben werden. Als Alternative ist es möglich Qualifier dynamisch mit Hilfe von Literals anzugeben. Dies ist auch hier wieder mit den entsprechenden select -Methoden möglich. Die Verwendung ist äquivalent zu den Konzepten, welche wir bei javax.enterprise.inject.Instance im vorherigen Kapitel kennengelernt haben.

 

In CDI-basierten Frameworks gibt es Events auf welche im Framework selbst nicht reagiert wird. Solche Events werden wir später in IdeaFork überwachen. Applikationsspezifische Events sind per Definition nur sinnvoll, wenn diese in der Applikation verarbeitet werden. Hierfür müssen wir in einem beliebigen CDI-Bean eine Methode mit mindestens einem Parameter hinzufügen. Der Parametertyp und explizite/implizite Qualifier müssen jenen der Event-Quelle entsprechen. In unserem Fall ist die Idea -Klasse der Parametertyp. Listing CDI-Events überwachen zeigt, dass wir keinen expliziten Qualifier angeben müssen, da wir beim Feuern des Events den impliziten Default -Qualifier verwendet haben. Damit der CDI-Container die Methode als Observer-Methode registriert, muss dieser Parameter mit @Observes annotiert werden.
@ApplicationScoped
public class IdeaSavedObserver {
  private static final Logger LOGGER =
    Logger.getLogger(IdeaSavedObserver.class.getName());

  private boolean isIdeaLoggingEnabled;

  @PostConstruct
  protected void init() {
    isIdeaLoggingEnabled = LOGGER.isLoggable(Level.FINE);
  }

  public void onIdeaSavedEvent(@Observes Idea savedIdea) {
    if (isIdeaLoggingEnabled) {
      LOGGER.fine("saved idea: " + savedIdea.getId());
    }
  }
}
Observer sind in vielen Fällen zustandslos und führen vor der Ausführung der eigentlichen Logik die erforderlichen Auswertungen durch. Dennoch ist es sinnvoll und vor allem möglich unveränderbare Informationen bei der Initialisierung abzulegen, damit die Ausführungszeit der Observer-Methode minimiert werden kann. In unserem einfachen Beispiel könnten diese Informationen statisch abgelegt werden. Normalerweise handelt es sich nicht ausschließlich um statische Informationen, wodurch eine mit @PostConstruct annotierte Methode zur Initialisierung sinnvoll ist. In application-scoped Beans, wie es IdeaSavedObserver ist, werden solche Auswertungen daher nur einmal und nicht bei jedem Aufruf der Observer-Methode/n durchgeführt. Denn die Observer-Methode wird auf der effektiven Contextual-Instance aufgerufen, als wäre es ein manueller Methoden-Aufruf auf das CDI-Bean.

 

Events sind primär sinnvoll, wenn wir potentiell mehrere unabhängige Aktionen im System anstoßen möchten. Zustandslose Observer werden oft dependent-scoped definiert. Bei jedem Event wird per Definition eine Contextual-Instance der Observer-Klasse erzeugt und die Observer-Methode aufgerufen. Abhängig von der effektiven Logik der Observer-Methode sind solche Methoden etwas schwerer testbar, da das dependent-scoped Bean nach dem Aufruf der Observer-Methode wieder zerstört wird. Im Hinblick auf die Zustellung von Events müssen Sie sich in solchen Fällen auf den CDI-Container verlassen. Ob ein Event effektiv gefeuert wird, kann bei Bedarf mit einem eigenen Test-Observer überprüft werden. Solche Test-Observer werden im Testverzeichnis abgelegt und dürfen nicht dependent-scoped sein. Die Parameter der Observer-Test-Methoden sollten jenen der echten Observer-Methode entsprechen, um eine sinnvolle Aussage zu ermöglichen. Die Implementierung ist hingegen in jedem Fall trivial, da Sie sich nur merken müssen, ob die Observer-Methode nach einem bestimmten Punkt aufgerufen wurde. Listing Zustellung von CDI-Events testen zeigt einen einfachen Testfall und Listing Test-Observer den dazugehörigen Test-Observer. Vor dem Speichern der neu erzeugten Idea -Instanz wird der injizierte TestIdeaSavedObserver überprüft. Bis zu diesem Zeitpunkt darf unser Event noch nicht aufgetreten sein. Nach der Speicherung wird der Zustand von TestIdeaSavedObserver erneut überprüft, weil das Event bereits erzeugt und vom CDI-Container zugestellt wurde.

 

Um zu vermeiden, dass das Event durch einen anderen Test bereits angestoßen und vom TestIdeaSavedObserver aufgezeichnet wurde, definieren wir TestIdeaSavedObserver als request-scoped Bean.
Tipp: Wie im Kapitel Einführung in CDI erwähnt, ist der CDI-Request-Scope unabhängig von einem physischen HTTP-Request, sofern der Scope für den aktuellen Thread manuell gestartet und gestoppt wird. In Unit-Tests ohne CDI-Support müssten wir dies manuell durchführen. In unserem Fall übernimmt dies CdiTestRunner für jede Test-Methode.
@RunWith(CdiTestRunner.class)
public class EventTest {
  @Inject
  private IdeaManager ideaManager;

  @Inject
  private IdeaRepository ideaRepository;

  @Inject
  private TestIdeaSavedObserver ideaSavedObserver;

  @Test
  public void eventDelivery() {
    Idea newIdea = ideaManager.createIdeaFor(...);

    Assert.assertFalse(ideaSavedObserver.isEventObserved());
    ideaRepository.save(newIdea);
    Assert.assertTrue(ideaSavedObserver.isEventObserved());
  }
}
@RequestScoped
public class TestIdeaSavedObserver {
  private boolean isEventObserved;

  protected void onIdeaSavedEvent(@Observes Idea savedIdea) {
    isEventObserved = true;
  }

  public boolean isEventObserved() {
    return isEventObserved;
  }
}

2.7.0.1 Ausführungsreihenfolge

Zusammen mit dem Test-Observer haben wir mittlerweile mehrere Observer-Methoden für unser Idea -Event. Die Ausführungsreihenfolge dieser Methoden ist weder beeinflussbar, noch durch eine Regel definiert. Wirft eine der Observer-Methoden eine Exception, so werden die nachfolgenden Observer-Methoden nicht mehr ausgeführt. Eine mehrstufige Ausführung eines Events und eine damit verbundene definierte Reihenfolge kann nur manuell mithilfe von verschiedenen Qualifiern erreicht werden. In den meisten Fällen ist dies nicht erforderlich und würde hauptsächlich zu einer komplexeren Implementierung führen und das Ergebnis kommt manuellen Methodenaufrufen nahe.

2.7.0.2 Bedingte Überwachung von Events

Existiert noch keine Contextual-Instances eines Managed-Beans mit Observer-Methode, dann wird diese erzeugt sobald das Event gefeuert wird. Somit sind reine Observer-Beans möglich. Für manche Anwendungsfälle ist es hingegen erforderlich, dass eine Observer-Methode nur aufgerufen wird, wenn es bereits eine Contextual-Instance des Managed-Beans gibt. Ein Beispiel hierfür sind UI-Controller, welche ein bestimmtes UI-Event überwachen. Wird das gleiche Event für verschiedene Oberflächen verwendet, so ist es normalerweise nicht sinnvoll sämtliche UI-Controller der gesamten Applikation, welche eine Observer-Methode für dieses Event haben, zu erzeugen und die entsprechende Observer-Methode aufzurufen. In IdeaFork sind wir von solchen spezielleren Anwendungsfällen noch entfernt und somit veranschaulicht Listing Bedingter Test-Observer das grundlegende Konzept anhand eines einfachen Beispiels. Die neu hinzugefügte Klasse TestIdeaSavedConditionalObserver ist fast ident zur Implementierung von TestIdeaSavedObserver . Der einzige Unterschied ist die Angabe von Reception.IF_EXISTS . Da in Listing Zustellung von bedingten CDI-Events testen vor dem Speichern der Idee TestIdeaSavedConditionalObserver nicht verwendet wird, kommt es zu keiner Ausführung der bedingten Observer-Methode.
@RequestScoped
public class TestIdeaSavedObserver {
  private boolean isEventObserved;

  protected void onIdeaSavedEvent(
    @Observes(notifyObserver = Reception.IF_EXISTS)
    Idea savedIdea) {
      isEventObserved = true;
  }

  public boolean isEventObserved() {
    return isEventObserved;
  }
}
@RunWith(CdiTestRunner.class)
public class EventTest {
  @Inject
  private IdeaManager ideaManager;

  @Inject
  private IdeaRepository ideaRepository;

  @Inject
  private TestIdeaSavedObserver ideaSavedObserver;

  @Inject
  private TestIdeaSavedConditionalObserver conditionalObserver;

  @Test
  public void conditionalEventDelivery() {
    Idea newIdea = ideaManager.createIdeaFor(...);

    Assert.assertFalse(ideaSavedObserver.isEventObserved());

    ideaRepository.save(newIdea);

    Assert.assertTrue(ideaSavedObserver.isEventObserved());
    Assert.assertFalse(conditionalObserver.isEventObserved());

    ideaRepository.save(newIdea);
    Assert.assertTrue(conditionalObserver.isEventObserved());
  }
}

2.8 Beans ersetzen

In einigen Fällen kann es sinnvoll sein bestehende Implementierungen anzupassen. Verwenden Sie ein externes CDI-basiertes Framework oder teilen sich mehrere Ihrer Applikationen CDI-basierte Module, dann kann es zu Situationen kommen, in welchen eine bestehende Implementierung erweitert oder ausgetauscht werden soll. Darüber hinaus kann es sein, dass Unit-Tests zusätzliche Anforderungen an einzelne Beans stellen. Für diese und einige andere Anwendungsfälle stellt CDI mit @Alternative und @Specializes gleich zwei Ansätze zur Verfügung.

2.8.0.1 Nichts ist perfekt

CDI ist eine überwiegend gute und durchdachte Spezifikation. Leider gibt es wie bei jeder Spezifikation bzw. Technologie Fallstricke. Im Falle von CDI verbergen sich die auffälligsten Probleme bei den BDAs (= Bean Deployment Archives), auf welche wir später noch eingehen werden. Die Auswirkungen werden vor allem bei alternativen Implementierungen, mit welchen wir uns in diesem Kapitel beschäftigen, sichtbar. Bereits bei der Definition der beiden Annotationen gibt es konträre Ansichten. Beispielsweise funktioniert javax.enterprise.inject.Instance in Verbindung mit @Alternative mit OpenWebBeans anders als mit Weld. In Weld liefert der Iterator das ursprüngliche und das alternative Bean als Ergebnis. Folglich gibt isAmbiguous "true" zurück. Allerdings führt die get -Methode hier nicht wie erwartet zu einer AmbiguousResolutionException , da nur das alternative Bean zurückgeliefert wird. OpenWebBeans implementiert dies konsistent und verwendet in beiden Fällen nur das alternative Bean und somit gibt isAmbiguous wie erwartet "false" zurück. In Kombination mit fehlerhaften Integrationen von CDI in manchen Applikationsservern, sind beide Annotationen nur sehr beschränkt einsetzbar. Statt auf sämtliche Regeln der Spezifikation und die vorhandenen Implementierungsfehler bzw. Inkonsistenzen in den einzelnen Versionen der CDI-Implementierungen einzugehen, beschränken wir uns auf die wichtigsten Anwendungsfälle, welche in den meisten Fällen portable sind und funktionieren.
Tipp: Wenn Sie Ihre Nerven schonen möchten, dann können Ihnen folgende Empfehlungen das Leben mit CDI 1.0 erleichtern. Beschränken Sie die Verwendung von @Alternative und @Specializes auf das absolute Minimum. Bevorzugen Sie @Specializes und erst wenn diese Annotation nicht funktioniert oder für Ihren Anwendungsfall nicht möglich ist, versuchen Sie @Alternative . Testen Sie die alternativen Implementierungen überdurchschnittlich gut. Wenn weder @Alternative noch @Specializes funktionieren, dann können Sie in vielen Fällen auf eine Funktionalität von Apache DeltaSpike namens "global alternatives" ausweichen.

2.8.0.2 Implementierungen ersetzen mit @Specializes

Wir beginnen mit @Specializes , weil es die einfachste Variante ist um eine Default-Implementierung auszutauschen. Die spezialisierte Implementierung leitet wie erwartet von der ursprünglichen Bean-Klasse ab und wird mit @Specializes annotiert. Falls die ursprüngliche Klasse ein Interface implementiert, so genügt es nicht nur das gleiche Interface zu implementieren, sondern es muss immer von der ursprünglichen Klasse abgeleitet werden.

 

In IdeaFork haben wir TestIdeaSavedObserver angelegt, um zu testen ob das Idea -Event zum richtigen Zeitpunkt gefeuert wird und wir die ursprüngliche Implementierung diesbezüglich nicht anpassen wollten. Die Anpassung für den Unit-Test war in unserem simplen Beispiel sehr einfach möglich. In komplexeren Fällen kann es jedoch sein, dass dies nicht so einfach möglich ist. Hier können Sie @Specializes zur Hilfe nehmen. Statt ein zusätzliches Bean zu verwenden, können wir TestIdeaSavedObserver von IdeaSavedObserver ableiten und entsprechend anpassen. Da wir TestIdeaSavedObserver im Testverzeichnis haben, wird diese Implementierung nur in unseren Unit-Tests aktiv. In der effektiven Applikation kann der Testcode von TestIdeaSavedObserver nie aktiv werden, weil die endgültige Applikation diese Testklasse nicht enthalten wird und daher autom. IdeaSavedObserver verwendet wird. Listing Spezialisierte Implementierungen zeigt, dass wir in unserem einfachen Fall die Observer-Methode überschreiben. Die neue Implementierung delegiert im ersten Schritt an die ursprüngliche Implementierung der Superklasse und fügt danach den erforderlichen Testcode ein.
@Specializes
@RequestScoped
public class TestIdeaSavedObserver extends IdeaSavedObserver {
  private boolean isEventObserved;

  @Override
  protected void onIdeaSavedEvent(@Observes Idea savedIdea) {
    super.onIdeaSavedEvent(savedIdea);
    this.isEventObserved = true;
  }

  public boolean isEventObserved() {
    return isEventObserved;
  }
}
In unserem Fall ist IdeaSavedObserver als application-scoped Bean definiert. Durch die Spezialisierung haben wir nicht nur die Implementierung erweitert, sondern auch den Scope des Beans verändert. Für eine spezialisierte Implementierung ist die Angabe eines Scopes nur erforderlich, wenn der Scope des Beans, wie bei TestIdeaSavedObserver , geändert werden soll. Wird kein Scope bei der spezialisierten Klasse angegeben, so wird die Scope-Annotation vererbt, sofern diese mit @java.lang.annotation.Inherited annotiert ist. Da dies eine Empfehlung der CDI-Spezifikation ist, sind alle Scope-Annotationen der Spezifikation mit @Inherited annotiert und werden autom. vererbt.

 

In vielen Fällen ist es nicht erforderlich den Scope zu ändern. Wenn wir uns zurück erinnern haben wir TestIdeaSavedObserver mit @RequestScoped annotiert, weil wir je Test-Methode eine neue Instanz verwenden wollen. Bevor Sie den Scope der ursprünglichen Implementierung verändern, stellen Sie sicher, dass Sie die Auswirkungen verstehen und Sie nicht wichtige Aspekte bzw. Annahmen der ursprünglichen Implementierung verletzen und dadurch unerwartete Effekte auslösen.

2.8.0.3 Alternative Implementierungen mit @Alternative

Im Vergleich zu den zuvor vorgestellten spezialisierten Implementierungen mit @Specializes sind alternative Implementierungen mit @Alternative etwas komplexer. Die Annotation selbst ist nur der erste Schritt. Wie bei @Specializes können Sie von der ursprünglichen Implementierung ableiten und diese mit @Alternative annotieren. In vielen Anwendungsfällen haben Sie allerdings nur ein Interface, welches Sie für das alternative Bean implementieren können/wollen. Dies stellt den Hauptanwendungsfall von @Alternative dar, weil mit @Specializes die alleinige Implementierung eines Interfaces nicht möglich ist. Ein weiterer Unterschied ist, dass Implementierungen welche mit @Alternative annotiert sind nicht autom. aktiv sind, da sie via beans.xml konfiguriert werden müssen. Hier lauert einer der Fallstricke der Spezifikation. Laut dieser müssen alternative Implementierungen im gleichen (Bean-Deployment-)Archiv konfiguriert sein. Die Definition eines (Bean-Deployment-)Archivs und dessen Grenzen sind sehr umstritten. Im restriktivsten Fall müssen Sie davon ausgehen, dass die kleinste Moduleinheit, wie bspw. eine JAR-Datei, gemeint ist. Dieses Konzept und die damit verbundenen Limitierungen werden wir im Kapitel CDI und Java EE genauer analysieren. Da wir nicht mehrere Applikationen haben, welche sich CDI-basierte Module teilen und die Anpassung eines CDI-basierten Frameworks erst später besprechen, werden wir zu diesem Zeitpunkt keine Einschränkungen bemerken.

 

@Alternative bietet Ihnen die Möglichkeit mehrere Implementierungen parallel auszuprobieren. Wird keine der alternativen Implementierungen via beans.xml aktiviert, so bleibt das ursprüngliche Bean aktiv. Sie können beliebig viele alternative Implementierungen mit @Alternative annotieren. Erst wenn Sie eine davon via beans.xml aktivieren, wird diese zur Laufzeit verwendet.

 

In IdeaFork bietet es sich an alternative Beans für das ObjectConverter Interface zu implementieren. Als Alternative zu JAXB und Gson können wir Jackson (http://fasterxml.com) in das Projekt einbinden und entsprechende Implementierungen für die JSON- und XML-Converter testen. Listing Alternative Implementierung zeigt dies am Beispiel eines Jackson-Converters für JSON. Listing Aktivierung einer alternativen Implementierung veranschaulicht die entsprechende Konfiguration in der Datei beans.xml .
@Alternative
@ExternalFormat(ExternalFormat.TargetFormat.JSON)
@ApplicationScoped
public class JSONConverterJackson implements ObjectConverter {
  @Override
  public <T> T toObject(String value, Class<T> targetType) {
    try {
      return new ObjectMapper().readValue(value, targetType);
    } catch (Exception e) {
      throw new IllegalArgumentException(e);
    }
  }

  @Override
  public String toString(Object entity) {
    try {
      return new ObjectMapper().writeValueAsString(entity);
    } catch (JsonProcessingException e) {
      throw new IllegalArgumentException(e);
    }
  }
}
<beans>
  <alternatives>
    <class>[package-name].JSONConverterJackson</class>
  </alternatives>
</beans>

2.8.0.4 Alternative Producer/Disposer

Producer und Disposer haben eine Art Sonderstellung in Hinsicht auf alternative Implementierungen. Die höchste Portabilität Ihrer Beans erreichen Sie, wenn Sie wie in Listing Spezialisiertes Bean mit Producer und Disposer in der abgeleiteten Klasse sowohl den Producer als auch den Disposer inklusive aller Metadaten überschreiben, selbst wenn Sie an diesen Stellen keine Anpassungen vornehmen und nur an die ursprüngliche Implementierung delegieren. In unserem Beispiel passen wir den Producer für ApplicationConfig an, da wir im nächsten Kapitel für Unit-Tests andere Werte konfigurieren werden.
@Specializes
public class TestConfigProducer extends ConfigProducer {
  @Override
  @Produces
  @ApplicationScoped
  public ApplicationConfig exposeConfig() {
    return super.exposeConfig();
  }

  @Override
  public void onDispose(@Disposes ApplicationConfig config) {
    super.onDispose(config);
  }

  @Override
  protected String getConfigBaseName() {
    return "test-" + super.getConfigBaseName();
  }
}
Darüber hinaus gibt es Unterschiede bei den CDI-Implementierungen. Mit OpenWebBeans können Sie sowohl @Specializes als auch @Alternative verwenden. Bei Weld sind Sie hingegen auf @Specializes beschränkt, um alternative Producer zur Verfügung zu stellen.

2.9 Interceptoren

Interceptoren ermöglichen die Implementierung von Querschnittsbelangen, wie bspw. Logging, Security, Monitoring u.v.m. unabhängig von der konkreten Arbeitsweise der Beans, für welche sie verwendet werden. Im Vergleich zu anderen Interceptor-Lösungen bzw. AOP sind CDI-Interceptoren einfach und elegant gehalten. Vergleichbar mit Servlet-Filtern wird eine verschachtelte Kette aus 2-n Instanzen erzeugt. Die kleinste Kette besteht aus einem Interceptor und der eigentlichen Contextual-Instance. Der Interceptor kann an beliebigen Stellen seiner Logik an die nächste Instanz in der Kette delegieren. Als letztes Glied in der Kette wird die Methode der Contextual-Instance aufgerufen. Danach wird wie bei verschachtelten Methodenaufrufen die Kette in die umgekehrte Richtung zurückgeschritten. Sie können auf einfache Weise entscheiden, welche Interceptor-Logik vor und nach dem Aufruf der effektiven Methode der Contextual-Instance ablaufen soll. Für Exceptions benötigen Sie keinen zusätzlichen Interceptortyp, da Sie den klassischen try/catch/finally-Block verwenden können.

 

In IdeaFork führen wir einen Interceptor ein, mit welchem wir langsame Methodenaufrufe aufzeichnen können. Wir können diesen Interceptor für beliebige Beans verwenden. Fürs Erste werden wir IdeaManager , IdeaRepository und alle ObjectConverter -Implementierungen mit diesem Interceptor überwachen.

 

Für die Implementierung eines Interceptors müssen wir eine eigene Annotation definieren. Ähnlich wie bei Qualifiern müssen wir überlegen an welchen Stellen wir den Interceptor verwenden wollen bzw. dürfen. Die CDI-Spezifikation erlaubt die Verwendung bei einzelnen Methoden sowie auf Klassenebene, wodurch sämtliche (Business-)Methoden der Klasse den Interceptor erhalten. Somit wird in Listing Interceptor-Annotation ElementType.METHOD und ElementType.TYPE verwendet. Erst durch die Annotation @javax.interceptor.InterceptorBinding markieren wir unsere neue Annotation als sog. Interceptor-Binding.
@InterceptorBinding

@Target({TYPE, METHOD})
@Retention(RUNTIME)
public @interface Monitored {}
Mit dieser Annotation können wir unsere Managed-Bean-Klassen oder einzelne Methoden annotieren. Listing Verwendung der Interceptor-Annotation veranschaulicht dies stellvertretend mit der Klasse IdeaRepository .
@Monitored
public class IdeaRepository {
  //...
}
Da Annotationen nur Metadaten sind, benötigen wir einen zweiten Teil des Interceptor-Bindings, die Interceptor-Implementierung selbst. Hierfür legen wir die Klasse MonitoredInterceptor an und annotieren sie mit @javax.interceptor.Interceptor , um die Klasse als Interceptor-Implementierung zu markieren. Der Name MonitoredInterceptor setzt sich aus dem Namen der Interceptor-Binding Annotation und dem Wort Interceptor zusammen und folgt einer optionalen, aber weit verbreiteten, Namenskonvention. Allerdings müssen wir noch die Verbindung zwischen unserem Interceptor-Binding und der Interceptor-Implementierung definieren. Aus diesem Grund annotieren wir die Interceptor-Implementierung mit unserer Interceptor-Binding Annotation. Bisher haben wir nur eine leere Klasse, welche mit Metadaten annotiert wurde. In Listing Interceptor-Implementierung ist ersichtlich, dass wir eine Interceptor-Methode benötigen, welche mit @javax.interceptor.AroundInvoke annotiert ist. Diese Methode kann einen beliebigen Namen haben, muss jedoch einen Parameter vom Typ InvocationContext verwenden, als Rückgabetyp Object definieren und die Möglichkeit bieten Exceptions zu werfen.
@Monitored
@Interceptor
public class MonitoredInterceptor implements Serializable {

  @Inject
  private ApplicationConfig applicationConfig;

  @AroundInvoke
  public Object intercept(InvocationContext ic) throws Exception {
    long start = System.currentTimeMillis();

    try {
      return ic.proceed();
    } finally {
      if (isSlowInvocation(start)) {
        //...
      }
    }
  }

  private boolean isSlowInvocation(long start) {
    return System.currentTimeMillis() - start >
      applicationConfig.getMethodInvocationThreshold();
  }
}
Eine Interceptor-Instanz übernimmt den Scope der entsprechenden Contextual-Instance. Somit gelten für Interceptor-Instanzen die gleichen Regeln wie für ein dependent-scoped Bean. Da Sie im Normalfall nicht wissen auf welches Bean der Interceptor angewandt wird, sollten Interceptoren immer Serializable implementieren. Die Minimalanforderungen an eine Interceptor-Implementierung schreiben die Implementierung des Serializable -Interfaces nicht vor, dennoch müssen Sie davon ausgehen, dass zur Laufzeit eine Interceptor-Instanz zusammen mit der Contextual-Instance serialisiert werden könnte.

 

In einem letzten Schritt müssen wir den Interceptor via beans.xml aktivieren, wie es in Listing Aktivierung einer Interceptor-Implementierung gemacht wird. Die Reihenfolge bei der Verschachtelung von mehreren Interceptoren wird nicht durch die Reihenfolge der Interceptor-Annotationen definiert, sondern durch die Auflistungsreihenfolge in der Konfigurationsdatei.
<beans>
  <interceptors>
    <class>[package-name].MonitoredInterceptor</class>
  </interceptors>
</beans>
Listing Interceptor-Implementierung hat außerdem gezeigt, dass eine Interceptor-Implementierung Injection-Points definieren kann. Durch diese Möglichkeit können wir eine Einschränkung von Interceptoren umgehen. Es ist nämlich nicht möglich Interceptor-Implementierungen auszutauschen, wie es bei CDI-Beans durch @Alternative bzw. @Specializes machbar ist. Wir können zwar hartcodiert eine andere Implementierung konfigurieren, aber wir werden in diesem Kapitel sowie im Kapitel zu portablen CDI-Erweiterungen sehen, dass ein flexiblerer Ansatz vergleichsweise viele Vorteile mit sich bringt.

 

Der Trick besteht darin, dass wir über einen Injection-Point eine Implementierung injizieren können, welche wie gewohnt mit @Alternative bzw. @Specializes anpassbar ist. Listing Interceptor-Strategy veranschaulicht die Umsetzung. Das separierte Bean implementiert äquivalent zur Interceptor-Implementierung das Interface Serializable . Da die ausgelagerte Implementierung dependent-scoped ist, wird kein Proxy erzeugt und der Overhead besteht primär aus der initialen Injizierung in die Interceptor-Instanz und ist somit minimal.

 

In IdeaFork können wir diesen Trick verwenden, um einen vorkonfigurierten Interceptor für Unit-Tests auszutauschen. Die alternative Implementierung der Interceptor-Strategy kann zusätzlichen Code für eine bessere Testbarkeit enthalten und in das Testmodul ausgelagert werden. Wir wollen eine produktive Implementierung natürlich nicht mit Test-Code belasten. In unserem Fall könnten wir ein Test-Bean mit diesem Interceptor versehen und eine entsprechend langsame Testmethode erstellen. Eine spezielle Interceptor-Implementierung, welche entsprechend gesteuert werden kann, ist in vielen Fällen einfacher.

 

Beginnen wir mit der Auslagerung der Interceptor-Logik selbst. Hierzu führen wir ein Interface namens MonitoredInterceptorStrategy ein. Bei der Definition der Methodensignatur folgen wir den Regeln für Interceptor-Methoden. Nur auf die @AroundInvoke -Annotation können wir an dieser Stelle verzichten. Listing Interceptor-Strategy zeigt das neue Interface und die Default-Implementierung mit unserer bisherigen Interceptor-Logik. In Listing Verwendung der Interceptor-Strategy sehen wir die angepasste Interceptor-Implementierung, welche an die aktive MonitoredInterceptorStrategy delegiert.
Tipp: Wir übernehmen die Bezeichnung InterceptorStrategy von populären CDI-Erweiterungen, welche wir in nachfolgenden Kapiteln kennenlernen werden. Es handelt sich um eine Namenskonvention, welche nicht von der CDI-Spezifikation vorgegeben wird.
public interface MonitoredInterceptorStrategy
  extends Serializable {
    Object intercept(InvocationContext ic) throws Exception;
}

@Dependent
public class DefaultMonitoredInterceptorStrategy
  implements MonitoredInterceptorStrategy {
    @Inject
    private ApplicationConfig applicationConfig;

    public Object intercept(InvocationContext ic)
      throws Exception {
        long start = System.currentTimeMillis();

        try {
          return ic.proceed();
        } finally {
          if (isSlowInvocation(start)) {
            //...
          }
        }
    }

    protected boolean isSlowInvocation(long s) {
      return System.currentTimeMillis() - s >
        applicationConfig.getMethodInvocationThreshold();
    }
}
@Monitored
@Interceptor
public class MonitoredInterceptor implements Serializable {

  @Inject
  private MonitoredInterceptorStrategy interceptorStrategy;

  @AroundInvoke
  public Object intercept(InvocationContext ic) throws Exception {
    return this.interceptorStrategy.intercept(ic);
  }
}
Im Zuge des Refactorings haben wir die Methode isSlowInvocation als protected definiert. Im nächsten Schritt können wir, wie in Listing Interceptor-Test-Strategy zu sehen ist, eine spezialisierte Implementierung für einen entsprechenden Unit-Test einführen. Listing Kontrolle der Interceptor-Test-Strategy zeigt, dass in einem Unit-Test das Ergebnis der überschriebenen Methode einfach gesteuert werden kann. Für unseren einfachen Test ist eine simple statische Variable in TestMonitoredInterceptorStrategy ausreichend. Für komplexere Fälle kann es erforderlich sein mit einem ThreadLocal zu arbeiten.
@Specializes
@Dependent
public class TestMonitoredInterceptorStrategy
  extends DefaultMonitoredInterceptorStrategy {
    private static boolean slowInvocationSimulationModeActive;

    @Override
    protected boolean isSlowInvocation(long start) {
      return slowInvocationSimulationModeActive;
    }

    static void activateTestMode(boolean newValue) {
      ControllableMonitoredInterceptorStrategy
        .slowInvocationSimulationModeActive = newValue;
    }
}
@RunWith(CdiTestRunner.class)
public class InterceptorTest {
  @Inject
  private IdeaManager ideaManager;

  @Inject
  private MonitoredStorage monitoredStorage;

  @After
  public void resetInvocationMode() {
    TestMonitoredInterceptorStrategy.activateTestMode(false);
  }

  @Test
  public void normalMethodInvocation() {
    ideaManager.createIdeaFor("", "");
    Assert.assertTrue(monitoredStorage.getSlowMethods().isEmpty());
  }

  @Test
  public void slowMethodInvocation() {
    TestMonitoredInterceptorStrategy.activateTestMode(true);
    Assert.assertTrue(monitoredStorage.getSlowMethods().isEmpty());
    ideaManager.createIdeaFor("", "");
    Assert.assertFalse(monitoredStorage.getSlowMethods().isEmpty());
  }
}

2.9.0.1 Interceptoren mit Zusatzinformationen

Äquivalent zu Qualifiern können auch Interceptor-Annotationen mit Annotation-Attributen versehen werden. Ohne @Nonbinding werden Annotation-Attribute, genau wie bei Qualifiern, zum Auffinden der passenden Interceptor-Implementierung verwendet. Zusätzlich ist es möglich eine Interceptor-Implementierung mit mehreren Interceptor-Binding Annotationen zu versehen. Über diesen Mechanismus können Sie eine Interceptor-Implementierung an eine bestimmte Interceptor-Kombination binden. In der Praxis sind die Anwendungsfälle für beide Konzepte sehr dünn gesät. Für Interceptoren wesentlich interessanter sind die Verwendungsmöglichkeiten der @Nonbinding -Annotation. Sie ermöglicht bspw. globale Konfigurationen mit Inline-Konfigurationen zu übersteuern.

 

In IdeaFork injizieren wir ApplicationConfig in DefaultMonitoredInterceptorStrategy , um einen konfigurierten Wert auszulesen. Im konkreten Fall legen wir über die Konfiguration die maximal erwartete Ausführungszeit einer durchschnittlichen Methode fest. Überschreitet ein Methodenaufruf diesen Wert, wird dies mit Hilfe von MonitoredStorage aufgezeichnet. Es kann natürlich Ausnahmen geben, von denen wir wissen, dass dieser Durchschnittswert immer überschritten wird oder die maximale Dauer niedriger bemessen werden soll. Für Fälle wie diesen können wir ein mit @Nonbinding annotiertes Annotation-Attribut hinzufügen, um einen anderen Wert für solche Ausnahmefälle festzulegen. Listing Interceptor mit Zusatzinformation zeigt die Erweiterung der Interceptor-Binding Annotation und Einfache Auswertung von Interceptor-Informationen die Auswertung dieser zusätzlichen Information. Die hier gezeigte einfache Auswertung setzt voraus, dass die Interceptor-Annotation physisch entweder auf der intercepteten Methode oder deren Klasse verfügbar ist. Wie wir im Kapitel Portable CDI-Erweiterungen sehen werden, können Metadaten wie Interceptoren bei Bedarf dynamisch während des Containerstarts hinzugefügt werden. Hierfür müssten wir uns die Managed-Bean Definition über den BeanManager suchen, da diese die effektive Bean-Definition zur Laufzeit enthält.
@InterceptorBinding

@Target({TYPE, METHOD})
@Retention(RUNTIME)
public @interface Monitored {
  @Nonbinding
  int maxThreshold() default -1;
}
public class DefaultMonitoredInterceptorStrategy
  implements MonitoredInterceptorStrategy {
    //...
    @Inject
    private ApplicationConfig applicationConfig;

    @Override
    public Object intercept(InvocationContext ic) throws Exception {
      long start = System.currentTimeMillis();

      try {
        return ic.proceed();
      } finally {
        Monitored monitored = extractMonitoredAnnotation(ic);
        int maxThreshold = monitored.maxThreshold();

        if (maxThreshold < 1) {
          maxThreshold = applicationConfig.getMethodInvocationThreshold();
        }
        if (isSlowInvocation(start, maxThreshold)) {
          //...
        }
      }
    }
  }
  private Monitored extractMonitoredAnnotation(InvocationContext ic) {
    Monitored result = ic.getMethod().getAnnotation(Monitored.class);

    if (result == null) {
      result = ic.getTarget().getClass()
        .getAnnotation(Monitored.class);
    }
    if (result == null) {
      result = ic.getTarget().getClass().getSuperclass()
        .getAnnotation(Monitored.class);
    }
    return result;
  }
  protected boolean isSlowInvocation(long start, int maxThreshold) {
    return System.currentTimeMillis() - start > maxThreshold;
  }
}

2.10 Decoratoren

Abgesehen von generischen Querschnittsbelangen, welche mit Interceptoren im vorherigen Kapitel erklärt wurden, ist es mit CDI möglich maßgeschneiderte fachliche Interceptoren zu erstellen. In Applikationen sind die sog. Decoratoren, im Vergleich zu Interceptoren, eher selten zu finden.

 

Um den möglichen Vorteil von Decoratoren etwas klarer darzustellen, erweitern wir IdeaFork bevor wir unseren ersten Decorator implementieren. Bis auf die Typisierung auf Idea ist die Implementierung von IdeaRepository generisch. Eine tatsächlich generische Variante könnten wir für andere Entitäten wiederverwenden. Bisher haben wir in IdeaFork keine Entität, welche einen User repräsentiert und fügen daher eine gleichnamige Entität hinzu. Mit Generics und entsprechenden Interfaces und abstrakten Klassen können wir redundante Implementierungen verringern (siehe Repository Refactoring in IdeaFork ). Somit führen wir u.a. die abstrakte Klasse BaseEntity und das Interface GenericRepository ein. Listing Basis-Entität in IdeaFork veranschaulicht, dass wir in der Klasse BaseEntity außerdem ein neues Feld für die Version der Entität hinzufügen, da wir diese im nächsten Schritt verwenden werden. Die Entitäten Idea und User leiten von dieser neuen Basisklasse ab und erweitern sie um die entsprechenden Properties. In IdeaFork wollen wir einen Autor vom Typ User referenzieren. Aus diesem Grund müssen wir JAXB Annotationen hinzufügen, damit unsere bestehenden ObjectConverter -Tests weiterhin funktionieren.
public abstract class BaseEntity implements Serializable {
  protected String id;
  protected Long version;

  public BaseEntity() {
    this.id = UUID.randomUUID().toString();
  }

  public void increaseVersion() {
    if (version == null) {
      version = 0L;
    } else {
      version++;
    }
  }
  //...
}
public interface GenericRepository<T extends BaseEntity>
  extends Serializable {
    void save(T entity);
    void remove(T entity);
    T loadById(String id);
}

public interface IdeaRepository extends GenericRepository<Idea> {}

public abstract class GenericInMemoryRepository<T extends BaseEntity>
  implements GenericRepository<T> {
    protected Map<String, T> entityMap =
      new ConcurrentHashMap<String, T>();

    public void save(T entity) {
      entity.increaseVersion();
      this.entityMap.put(entity.getId(), clone(entity));
    }
    public void remove(T entity) {
      this.entityMap.remove(entity.getId());
    }
    public T loadById(String id) {
      T originalEntity = this.entityMap.get(id);
      T detachedEntity = null;

      if (originalEntity != null) {
        detachedEntity = clone(originalEntity);
      }
      return detachedEntity;
  }
  public static <T> T clone(T source) { /*...*/ }
}

@Monitored
public class IdeaInMemoryRepository extends GenericInMemoryRepository<Idea>
  implements IdeaRepository {}
Da wir Idea nur vorübergehend als Event-Typ verwendet haben und dies spätestens jetzt nicht mehr sinnvoll ist, erstellen wir die Klasse EntityChangedEvent , welche in Listing Explizite Event-Klasse zu sehen ist.
public abstract class EntityChangedEvent <T extends BaseEntity> {
  private final T entity;
  private final long creationTimestamp;

  public EntityChangedEvent(T entity) {
    this.entity = entity;
    this.creationTimestamp = System.currentTimeMillis();
  }

  public T getEntity() {
    return entity;
  }

  public long getCreationTimestamp() {
    return creationTimestamp;
  }
}

public class UserChangedEvent extends EntityChangedEvent<User> {
  public UserChangedEvent(User createdEntity) {
    super(createdEntity);
  }
}
Mit diesem Stand sind wir bereit einen sinnvollen Decorator zu erstellen. GenericInMemoryRepository enthält nur noch die effektive Logik für die Verwaltung von Entitäten und nicht mehr die Erzeugung von Events. Dies ist der Grund, warum zu diesem Punkt unseres Refactorings die Tests in EventTest fehlschlagen. Die Event-Behandlung werden wir im nächsten Schritt in einen Decorator auslagern.

 

Durch das neue Interface GenericRepository können wir eine abstrakte Klasse GenericRepositoryDecorator erstellen, da wir ein Interface als Basis benötigen. Die abstrakte Klasse kann für alle GenericRepository -Decoratoren als Basis-Klasse verwendet werden. Hierfür implementieren wir das zu dekorierende Interface selbst und fügen an den entsprechenden Stellen die benötigte Decorator-Logik hinzu. In unserem Fall implementieren wir mit der Methode checkEntity eine grundlegende Überprüfung, welche wir ausführen bevor an die dekorierte Instanz delegiert wird. Somit können wir rudimentäre technische Überprüfungen von der Repository-Implementierung trennen. Außerdem rufen wir nach einer erfolgreichen Speicherung die abstrakte Methode fireEntityChangedEvent auf. Konkrete Decorator-Implementierungen stellen diese zur Verfügung und können somit das jeweils benötigte Event feuern. Mit Hilfe der abstrakten Methode getDelegate können wir in der generischen Implementierung auf die dekorierte Instanz zugreifen, welche von einem konkreten Decorator zur Verfügung gestellt werden muss. Die wichtigsten Bestandteile von GenericRepositoryDecorator sind in Listing Generische Decorator Logik ersichtlich.
public abstract class GenericRepositoryDecorator
  <T extends BaseEntity> implements GenericRepository<T> {

    protected abstract GenericRepository<T> getDelegate();

    protected abstract void fireEntityChangedEvent(T entity);

    @Override
    public void save(T entity) {
      checkEntity(entity);
      getDelegate().save(entity);
      fireEntityChangedEvent(entity);
    }

    @Override
    public void remove(T entity) {
      checkEntity(entity);
      getDelegate().remove(entity);
    }

    @Override
    public T loadById(String id) {
      if (id == null) {
        throw new IllegalArgumentException("...");
      }
      return getDelegate().loadById(id);
    }

    private void checkEntity(T entity) {
      //...
    }
}
Listing Decorator Implementierung zeigt ein Beispiel für eine konkrete Decorator Implementierung. Äquivalent zu Interceptoren gibt es eine Marker-Annotation namens @Decorator , welche dem CDI-Container signalisiert, dass es sich bei dieser Klasse um eine Decorator-Implementierung handelt. Durch @Delegate in Kombination mit @Inject können wir uns die nächste Instanz in der Kette injizieren. Normalerweise muss der Typ des injizierten Delegates mit dem Decorator-Typ übereinstimmen. Wollen wir bspw. IdeaRepository dekorieren, so muss der Decorator IdeaRepository implementieren. Da GenericRepositoryDecorator und unsere Repositories in diesem Fall GenericRepository implementieren, ist dies aus technischer Sicht bereits ausreichend, da der korrekte Decorator über Generics identifiziert wird. Obwohl in diesem Fall die Implementierung des konkreten Repository-Interfaces technisch gesehen nicht erforderlich ist, sollte dies gemacht werden, da wir nur über diesen Weg Methoden dekorieren können, welche durch ein konkretes Repository -Interface und nicht durch das GenericRepository -Interface definiert werden.
@Decorator
public class IdeaRepositoryDecorator
  extends GenericRepositoryDecorator<Idea>
  implements IdeaRepository /*optional here*/ {

    @Inject
    @Delegate
    private IdeaRepository delegate;

    @Inject
    private Event<IdeaChangedEvent> changedEvent;

    protected IdeaRepository getDelegate() {
      return delegate;
    }

    @Override
    protected void fireEntityChangedEvent(Idea entity) {
        changedEvent.fire(new IdeaChangedEvent(entity));
    }
}
Listing Aktivierung einer Decorator-Implementierung stellt den letzten Schritt bei der Implementierung eines Decorators dar. Äquivalent zu Interceptoren müssen Decoratoren in der Datei beans.xml aktiviert werden. Sollte es für ein Bean mehrere Decoratoren geben, so kann die Verschachtelungsreihenfolge durch die Reihenfolge bei der Auflistung definiert werden. Damit die Tests in EventTest wieder funktionieren, müssen wir natürlich unsere Observer-Methoden noch von Idea auf IdeaChangedEvent umstellen.
<decorators>
  <class>[package-name].IdeaRepositoryDecorator</class>
</decorators>
Einen weiteren Vorteil, welchen wir durch die Auftrennung der Repository-Logik erhalten, werden wir sehen sobald wir im Kapitel CDI und Java EE alternative JPA-Repository-Implementierungen erstellen werden. Diese werden nämlich ebenfalls unsere eben erstellten Decorator-Implementierungen verwenden und müssen die grundlegenden Überprüfungen sowie die Event-Logik nicht redundant implementieren. Dies ist selbstverständlich nur als Beispiel gedacht, da in der Praxis oft nur eine Repository-Implementierung erstellt wird. Somit sollten Sie in Ihren Projekten genau evaluieren, ab wann sich die zusätzliche Komplexität durch die Einführung von Decoratoren lohnt.

2.10.0.1 Abstrakte Decoratoren

In vielen Fällen gibt es nicht für jede Methode Decorator-Logik. Decorator-Methoden ohne zusätzliche Logik würden einfach den Aufruf manuell weiterdelegieren. Daher erlaubt CDI die Verwendung von abstrakten Decorator-Klassen. Methoden, die nicht durch einen abstrakten Decorator implementiert sind, werden autom. an die nächste Instanz in der Kette weitergeleitet. In IdeaFork können wir EntityChangeRepository hinzufügen, um Änderungen an Entitäten zu archivieren. Listing Abstrakter Decorator veranschaulicht, dass nur eine Methode im dazugehörigen abstrakten Decorator implementiert und mit Decorator-Logik ausgestattet ist.
@Decorator
public abstract class EntityChangeRepositoryDecorator
  implements EntityChangeRepository {
    @Inject
    @Delegate
    private EntityChangeRepository delegate;

    @Override
    public void save(EntityChange entity) {
      checkEntity(entity);
      this.delegate.save(entity);
    }

    private void checkEntity(EntityChange ec) {
      //...
    }
}
EntityChangeRepository definiert außerdem die Methode findRevision , welche mit der ID einer Entität und der Versionsnummer den dazugehörigen archivierten Stand der Entität sucht. Die einzelnen Zustände der Entität können wir mit einem Observer autom. speichern. Listing Observer zur Speicherung von Revisionen zeigt die Observer- Implementierung in der Klasse IdeaHistoryProcessor .
@ApplicationScoped
public class IdeaHistoryProcessor {
  @Inject
  private ObjectConverter currentObjectConverter;

  @Inject
  private EntityChangeRepository entityChangeRepository;

  public void onIdeaCreated(
    @Observes IdeaChangedEvent changedEvent) {
      Idea entity = changedEvent.getEntity();
      String ideaSnapshot = currentObjectConverter.toString(entity);
      EntityChange entityChange =
        new EntityChange(
            entity.getId(),
            entity.getVersion(),
            ideaSnapshot,
            changedEvent.getCreationTimestamp());
      entityChangeRepository.save(entityChange);
  }
}
Bisher haben wir unseren Repository-Implementierungen keinen Scope vergeben. Da wir die Contextual-Instance immer nur in unseren Unit-Tests injiziert und angesprochen haben, sind die damit verbundenen Einschränkungen bisher nicht aufgefallen. EntityChangeRepository verwenden wir hingegen in der Klasse IdeaHistoryProcessor und Listing Ergebnis der Observer-Logik überprüfen veranschaulicht einen Ausschnitt aus einem Unit-Test, welcher die zweite Verwendung darstellt. Aus diesem Grund muss EntityChangeInMemoryRepository einen expliziten Scope erhalten. In unserem Fall entscheiden wir uns für den Application-Scope.
@RunWith(CdiTestRunner.class)
public class DecoratorTest {
  //...

  @Test
  public void passingGenericDecoratorCheck() {
    //...
    Idea newIdea = ideaManager.createIdeaFor(...);
    ideaRepository.save(newIdea);

    Idea savedIdea = ideaRepository.loadById(newIdea.getId());

    EntityChange revision = entityChangeRepository.findRevision(
      savedIdea.getId(), savedIdea.getVersion());

    Assert.assertNotNull(revision);

    Idea archivedIdea = objectConverter.toObject(
      revision.getEntityState(), Idea.class);
    Assert.assertEquals(savedIdea, archivedIdea);
  }
}

2.11 Stereotypes

Im vorherigen Kapitel haben wir herausgefunden, dass unsere Repository-Implementierungen einen Scope haben sollten. Im aktuellen Stand von IdeaFork haben wir jedoch eine Inkonsistenz. EntityChangeRepository ist application-scoped und die anderen Repository-Implementierungen sind nur mit @Monitored annotiert und somit implizit dependent-scoped. Mit CDI-Stereotypen ist es möglich solche Inkonsistenzen zentral zu lösen und Annotationen ausdrucksstärker und effizienter zu nutzen.

 

Vergleichbar mit UML-Stereotypen können mit CDI-Stereotypen Rollen eines Managed-Beans zum Ausdruck gebracht werden. Zusätzlich ermöglichen Stereotypen Informationen zu kapseln. Statt gleichartige Managed-Beans mit mehreren und vor allem immer gleichen Annotationen zu versehen, können wir eine Stereotyp-Annotation definieren, welche diese Annotationen kapselt. Die Annotation sollte nach dem Artefakt-Typ benannt werden. In unserem Fall legen wir eine Stereotyp-Annotation namens @Repository an und annotieren diese, wie in Listing Eigene Stereotyp-Annotation , mit @ApplicationScoped und @Monitored . Damit der CDI-Container unsere Stereotyp-Annotation als solche erkennt, muss diese mit @Stereotype annotiert werden. Wir verwenden in IdeaFork keine Producer für Repository-Implementierungen und daher genügt ElementType.TYPE für @Target .
@Target(TYPE)
@Retention(RUNTIME)

@Stereotype
@ApplicationScoped
@Monitored
public @interface Repository {}
Listing Verwendung einer eigenen Stereotyp-Annotation zeigt die vereinfachte Implementierung von EntityChangeInMemoryRepository .
@Repository
public class EntityChangeInMemoryRepository
  extends GenericInMemoryRepository<EntityChange>
  implements EntityChangeRepository {

    @Override
    public EntityChange findRevision(
      String entityId, long entityVersionToFind) {
        for (EntityChange current : entityMap.values()) {
          if (current.getEntityId().equals(entityId) &&
              current.getEntityVersion() == entityVersionToFind) {
                return current;
          }
        }
        return null;
    }
}
Natürlich annotieren wir die anderen Repository-Implementierungen ebenfalls mit unserer neuen Stereotyp-Annotation. Zukünftige Änderungen, wie bspw. die Änderung des Scopes, können durch die Stereotyp-Annotation zentral für alle Implementierungen vorgenommen werden.

 

Stereotypen können, wie in Listing Eigene Stereotyp-Annotation , den Default-Scope eines Managed-Beans vorgeben. Dieser kann bei konkreten Implementierungen für spezielle Fälle überschrieben werden, indem der Scope bei der Implementierung explizit angegeben wird. Ähnliches gilt für den Namen eines Beans. @javax.enterprise.inject.Model ist ein Stereotyp, welcher in der CDI-Spezifikation definiert ist. Diese Stereotyp-Annotation ist mit @RequestScoped und @Named annotiert. Beans, welche mit @Model annotiert werden, bekommen autom. einen Namen auf Basis der Default-Namenskonvention. Soll ein mit @Model annotiertes Bean einen anderen Namen erhalten, so kann der Default-Name durch die explizite Verwendung von @Named übersteuert werden. Mehr Details zur Benennung von Managed-Beans werden wir im Kapitel CDI und Java EE betrachten.
Tipp: Im vorherigen Kapitel haben wir manuell die Interceptor-Annotation gesucht. Da wir @Monitored in @Repository verschoben haben, müssen wir die Logik zum Auffinden der Annotation entsprechend erweitern und alle Annotationen analysieren. In IdeaFork wird eine einfache Umsetzung illustriert. Wollen Sie umfangreichere Konstellationen, wie bspw. Stereotyp-Annotationen annotierte mit einer oder mehreren anderen Stereotyp-Annotation/en, unterstützen, so muss die Suchlogik aufwändiger umgesetzt werden.

2.11.0.1 Alternative-Stereotypen

Bei der Verwendung von alternativen Implementierungen haben wir bisher jede Klasse separat in der Datei beans.xml aktiviert. In großen Applikationen mit vielen alternativen Beans kann dies sehr aufwändig und fehleranfällig werden. Aus diesem Grund stellt CDI einen einfachen und ausdrucksstarken Mechanismus auf Basis von Stereotypen zur Verfügung. Eine eigene Annotation, welche mindestens mit @Stereotype und @Alternative annotiert ist, ermöglicht die Aktivierung aller Alternativen, welche mit dieser Stereotyp-Annotation versehen sind.

 

Ein häufig anzutreffender Anwendungsfall ist die Aktivierung von Mock- bzw. Test-Klassen für Unit-Tests. Zur Abwechslung beginnen wir mit einem anderen Anwendungsfall. In IdeaFork möchten wir alle Jackson-Converter mit einem Konfigurationseintrag de-/aktivieren können. Die Annotation @JacksonConverter , welche in Listing Alternative Stereotyp-Annotation zu sehen ist, erhält außerdem die @Monitored Interceptor-Binding Annotation, sowie @ApplicationScoped als Default-Scope. Listing Aktivierung alternativer Beans via Stereotyp veranschaulicht die Aktivierung aller Jackson-Converter Implementierungen mit nur einem Eintrag in der Datei beans.xml .
@Target(TYPE)
@Retention(RUNTIME)

@Alternative
@Stereotype
@ApplicationScoped
@Monitored
public @interface JacksonConverter {}
<beans>
  <alternatives>
    <stereotype>[package-name].JacksonConverter</stereotype>
  </alternatives>
</beans>

2.12 Explizite Typisierung

Direkte typsichere Dependency-Injection bedeutet, dass es nur ein Managed-Bean je Typ geben kann. Wir haben bereits Qualifier als Erweiterung des Typsystems und die Möglichkeit von dynamischen Lookups kennengelernt. Mit diesen Konzepten ist es möglich die Einschränkungen von typsicherer Dependency-Injection etwas auszudehnen. Es gibt allerdings Fälle in denen ein Bean aus Sicht von Java mehrere Typen haben kann und für den CDI Injection-Prozess nur eine Teilmenge davon erhalten soll, um bspw. Typ-Überschneidungen mit anderen Beans zu verhindern.

 

In IdeaFork können wir bspw. die Anzahl der erforderlichen Injection-Points reduzieren, wenn wir aus IdeaManager und UserManager eine Facade machen. Beide Klassen können die dazugehörigen Repository-Interfaces implementieren und die entsprechenden Methoden an das jeweilige Repository delegieren. Implementiert bspw. IdeaManager das Interface IdeaRepository , so haben wir in der Applikation zwei Managed-Beans vom Typ IdeaRepository . Doppeldeutigkeiten wie diese führen zu einem Fehlstart der Applikation und eine AmbiguousResolutionException ist das Ergebnis.

 

Bei einem Injection-Point vom Typ IdeaRepository weiß der CDI-Container durch unsere gewünschte Änderung nicht, ob IdeaManager oder IdeaInMemoryRepository injiziert werden soll. In unserem Fall möchten wir IdeaInMemoryRepository injizieren, da IdeaManager dieses ebenfalls für die Delegierung der Methodenaufrufe verwendet. Solche Mehrdeutigkeiten können mit der Annotation @Typed elegant aufgelöst werden. Sie ermöglicht die Angabe von beliebigen Typen eines Managed-Beans, welche für den CDI-Container für den Injection-Prozess verwendet werden sollen. Mit @Typed() kann eine Klasse sogar für das Typsystem von CDI unsichtbar gemacht werden. Das Managed-Bean selbst existiert weiterhin im Hintergrund, doch es hat aus Sicht des CDI-Containers keinen Typ und kann deshalb nicht gefunden werden. Nur @Named könnte noch dafür sorgen, dass ein solches Managed-Bean über dessen Namen gefunden werden kann. Diesen Trick haben wir bereits bei ApplicationConfig genutzt, damit wir für die Producer-Methode keinen zusätzlichen Qualifier benötigen. Abgesehen von der kompletten Entfernung der Typinformation ist es auch möglich 1-n Typen explizit zu definieren. Nur diese Typen werden für das Managed-Bean vom CDI-Container verwendet. Für unsere Manager-Facade können wir, wie in Listing Explizite Typisierung , die Manager-Klasse selbst angeben. Unsere typsicheren Injection-Points werden somit wieder eindeutig.
@ApplicationScoped
@Typed(IdeaManager.class)
public class IdeaManager implements IdeaRepository {
  //...
}
@Typed kann daher für komplexere Konstellationen mit mehreren Interfaces bzw. einer Basisklasse verwendet werden und ermöglicht bei Bedarf die Verwendung komplexer Typhierarchien.