CDI: Typsichere pluggable Injection

Nachdem im vorherigen Beitrag über CDI, die „Contexts Dependency Injection“ in Java EE 6, die Basics wie Deklaration und Injection einer Bean behandelt wurden, so liegt der Fokus diesmal auf den flexiblen Mechanismen, mit denen CDI genutzt werden kann. Die Beschreibung ist dabei nicht allumfassend, sondern soll nur einen Einblick bieten, welche Vorteile die Nutzung von CDI in Komponenten-Architektur und -Implementierung bringt.

Zur Demonstration wird ein kleiner Rechenautomat entwickelt, der nach und nach um mehrere Rechenoperationen erweitert wird. Ein abstraktes simples Beispiel, welches mehrere Standard-Technologien kombiniert: Bean Validation, JavaServer Faces 2 mit Facelets, und natürlich CDI. Die Codebeispiele sind teils sinnvoll verkürzt. Die komplette lauffähige Web Application (WAR) steht am Ende des Beitrags verlinkt als Download bereit.

Oberfläche aus dem Java EE 6 Baukasten

Zum Testen der Funktionen wird JSF2 genutzt, welches ebenfalls zum leichtgewichtigen Java EE 6 Web Profile gehört und somit neben CDI immer zur Verfügung steht. Die Oberfläche bietet die Möglichkeit zwei Zahlen einzugeben. Die Zahlen können anschliessend per Knopfdruck addiert werden. Alternativ können „alle Operationen“ ausgeführt werden – was sich hierhinter versteckt, lichtet sich später. Die Seite wird standardkonform per XHTML (seit JSF2) wie folgt definiert:

<h:form>
    Value1:
    <h:inputText value="#{calculationService.input.value1}" 
        id="input1"/>
    <h:message for="input1" />

    Value2:
    <h:inputText value="#{calculationService.input.value2}"
        id="input2"/>
    <h:message for="input2" />

    <h:commandButton
        action="#{calculationService.performAddition}"
        value="addition" />
    <h:commandButton
        action="#{calculationService.performAll}"
        value="all operations" />
</h:form>
<ui:repeat value="#{calculationService.output}" var="line">
    <h:outputText value="#{line}" />
</ui:repeat>

Es werden zwei Eingabefelder h:inputText definiert, welche beide an Properties einer Bean calculationService.input gebunden werden. Über h:commandButton werden die angesprochenen Features, nämlich Addition und „alle Operationen“, angeboten. Schlussendlich werden über den Facelets-Loop ui:repeat die Ausgabezeilen aus dem calculationService ausgelesen und dargestellt. Vielleicht ist es aufgefallen: Für beide Eingabefelder gibt es jeweils ein h:message Block. Diese Blöcke stellen eventuell aufgetretene Validierungsmeldungen attributsspezifisch dar.

Validation Constraints im Entity

Die Fehlermeldungen der Validierung werden zwar automatisch generiert, erfordern jedoch Validierungsregeln. Daher ein Blick auf die Bean, in welche über die Eingabefelder geschrieben wird:

public class CalculationValue implements Serializable {
    @NotNull
    private Long value1;
    @NotNull
    private Long value2;
}

Man sieht hier Standard JSR-303 Bean Validation Annotations. In diesem Fall wird geprüft, ob in den Werten der Bean überhaupt Daten hinterlegt wurden. Die Bean Validation gehört ebenfalls zum Java EE 6 Web Profile. JSF2 prüft in der Process Validation Lifecycle Phase auf Bean Validation Constraints. Die Constraints werden dabei direkt in der Entity definiert, genau wie in diesem Beispiel. Zusätzlich wird die Bean Validation auch in nachgelagerten Schichten, zum Beispiel in einem JPA2 Persistency Layer, sichergestellt.

Addition per CDI

Die CalculationValue Entity ist als Property input in der Bean calculationService hinterlegt. Ausserdem werden bei den Button-Actions Methoden aus der Bean calculationService aufgerufen. Höchste Zeit also, einen Blick hinein zu werfen:

@Named
@SessionScoped
public class CalculationService implements Serializable {

    private List<String> output;

    @Inject
    private transient Addition addition;

    @Inject
    @LargeValue
    private CalculationValue input;

    public String performAddition() {
        output.clear();
        output.add( addition.operate(input) );
        return "index";
    }

Der CDI-geschulte Betrachter sieht natürlich sofort, dass es sich hierbei um eine Bean aus dem CDI Kontext handelt. Per @SessionScoped wird ihre Lebensdauer auf die Benutzer-Session erweitert. Dies ist nur für dieses Beispiel so gewählt und keine Voraussetzung. Der Standard @RequestScoped bzw. keine Angabe funktioniert ebenso. Die Bean kann ohne weitere Konfiguration direkt im JSF2 EL genutzt werden.

Als nächstes fällt die Injection @Inject einer Addition Bean auf. Diese Bean kapselt die Logik der Addition. Genau diese Logik wird in der performAddition() Methode aufgerufen. Dabei wird der Addition der Eingabewert input übergeben, welcher zuvor aus der JSF2 View gesetzt wurde. Die Addition wird dabei value1 und value2 aus input addieren. Klingt logisch, oder?

Eine Bean, viele Produzenten

Das Datenmodell input ist eine Instanz vom Typ der oben eingeführten CalculationValue Entity. Dafür benötigt man kein CDI, eine normale Deklaration ohne Annotations wäre völlig ausreichend. Trotzdem wird dem Member @Inject und @LargeValue vorangestellt. In diesem Beispiel ist das Ziel, eine explizite Initialisierung des Members zu erreichen. Die Werte von input werden zwar vom JSF2 Formular beim Submit gesetzt, allerdings werden bereits bei der Anzeige des Formulars die aktuellen Werte von input dargestellt. Es handelt sich hier also schlicht um das Setzen von Default Werten.

An dieser Stelle wird eine Entkopplung mittels CDI etabliert. Genauer gesagt sieht man hier einen Weg, das Factory-Pattern zu ersetzen. Durch das @Inject sucht CDI nach Möglichkeiten, die gewünschte Bean zu instanziieren. Das kann wie bereits geschehen per @Named direkt in der Bean-Klasse ermöglicht werden, eine mächtigere Variante aber ist, eine Methode zur Instanziierung zu nutzen:

@Named
public class ValueProducer {

    @Produces
    @LargeValue
    public CalculationValue initLarge() {
        final CalculationValue input = new CalculationValue();
        input.setValue1(2048L);
        input.setValue2(1024L);
        return input;
    }
    
    @Produces
    @SmallValue
    public CalculationValue initSmall() {
        final CalculationValue input = new CalculationValue();
        input.setValue1(6L);
        input.setValue2(2L);
        return input;
    }    
}

Diese Klasse (selbst CDI Bean) bietet zwei Methoden, welche beide mit @Produces annotiert sind. Durch diese Annotation weiss CDI, dass die Methoden CDI-Beans vom jeweiligen Return-Typ der Methode (hier: CalculationValue) herstellen kann. Dieses Wissen wird bei Injections genutzt, sofern keine passende Bean-Instanz bereits im Kontext vorhanden ist. Das heisst: Wenn eine @Inject Annotation gefunden wird, wird zuerst im Kontext geprüft ob bereits eine passende Bean Instanz vorliegt. Falls nein, wird die @Produces annotierte Methode aufgerufen und die dort erzeugte Bean injected. Dabei kann in der Methode natürlich auch komplexe Logik zur Generierung der Bean angewandt werden.

Nun wurden im ValueProducer jedoch zwei Methoden mit @Produces versehen. Und zu allem Unglück geben sogar beide Methoden den gleichen Typ zurück, beides mal CalculationValue! Welche Methode wird denn nun aufgerufen?

Bean-Auslese

Dieser Fall ist gar nicht so abwegig wie man denken könnte. Man stelle sich zum Beispiel vor: Zwei Datenbanken werden von einer Applikation genutzt. Nun könnte man Connection Instanzen injecten. Je nach Fall muss die Connection dann zur ersten oder zur zweiten Datenbank verbinden; trotzdem sind beide Klassen vom selben Typ.

CDI bietet hier eine elegante, weil typsichere Lösung. Im Beispiel werden die zwei produzierenden Methoden mit den Annotationen @LargeValue und @SmallValue unterschieden. Die gleichen Annotationen werden dann bei der Injection der Bean benutzt. Somit kann gesteuert werden, welche Methode zur Instanziierung genutzt wird. Trotzdem bleibt die Entkopplung erhalten: Der ValueProducer wird von keiner Klasse importiert.

Was aber ist @LargeValue eigentlich für eine Annotation? Eine eigene! Und die sieht so aus:

@Target( { ElementType.TYPE, ElementType.METHOD, 
ElementType.FIELD, ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Qualifier
public @interface LargeValue {
}

Wie man sieht, hält sich der Umfang doch in Grenzen. Aus CDI ist hierbei nur der @Qualifier Bezeichner, welcher CDI signalisiert dass diese Annotation zur Typisierung einer Bean dient. Der Code von @SmallValue sieht, bis auf den Namen, identisch aus.

Mittels @Produces Methoden kann also eine hohe Dynamik erreicht werden. Um trotzdem gezielt die passenden Beans herauszufiltern, bieten sich eigene @Qualifier an. Dies erhöht auch die Lesbarkeit des Codes, da die eigenen Annotationen auch entsprechend der Business Domain benannt werden können, z.B. @TopOffer.

Warum nicht einfach alles?

Eine noch höhere Flexibilität kann durch die Injection von Instanzlisten erzielt werden. Dies ist die gegenteilige Richtung von der gezielten Injection mittels @Qualifier : Es werden alle Beans im CDI Kontext eines Typs injected. Standard-Case für dieses Feature ist ein Plugin Mechanismus: Es kann eine Library eingebunden werden, welche das entsprechende Interface implementiert und sich dem CDI Kontext z.B. per @Named bekannt macht. Diese Implementierung ist dann überall verfügbar, ohne Wissen über die eigentliche Klasse oder Library.

Eine Erweiterung des Beispiels bringt dies näher. Der Rechenautomat soll jetzt endlich sein „alle Operationen“ Feature erhalten. Dabei werden alle im Automat verfügbaren Rechenoperationen nacheinander angewandt. Bisher ist dies nur die Addition, daher wird der Automat nun um neue Beans für Subtraktion, Multiplikation und Division erweitert. Alle diese Beans (auch die bereits existierende Addition) implementieren folgendes Interface:

public interface CalculationOperation {
    public String operate(CalculationValue input);
}

Es gibt nun vier CalculationOperation Implementierungen, welche alle via @Named im CDI Kontext als Bean registriert sind.

Diese Implementierungen werden gesammelt im CalculationService herangezogen und in der JSF Action Methode genutzt:

    ...
    @Inject
    private transient
        Instance<CalculationOperation> operations;
    
    public String performAll() {
        output.clear();
        for (CalculationOperation operation: operations) {
            output.add(operation.operate(input));
        }
        return "index";
    }

Instance ist ein Generic aus CDI. Die Injection mittels Instance zieht alle Instanzen im CDI Kontext dieses Typs heran. Weil hier ein Interface genutzt wird, werden also alle Implementierungen dieses Interfaces injected. Eine zusätzliche Einschränkung mit @Qualifier ist übrigens auch noch möglich. Nun kann man einfach über die Liste der Instanzen iterieren und die Methoden des Interfaces nutzen. Die Klasse CalculationService hat trotzdem keinerlei Referenzen auf eine der Implementierungen des CalculationOperation Interface. Es könnte nun zum Beispiel ein JAR mit zusätzlichen Implementierungen hinzugefügt werden, diese Operationen würden dann automatisch an dieser Stelle ausgeführt.

Application und Codebeispiele Download

Alle Beispiele als ausführbare Web Application (WAR) einschliesslich Quellcode finden Sie hier als Download (ZIP, 13kB). Zur Ausführung wird ein Java EE 6 Application Server benötigt (Web Profile möglich), zum Beispiel den GlassFish Server 3.0.

Das Ende und doch nur am Anfang

CDI bietet noch vieles mehr als in den Blog-Postings gezeigt werden konnte. Interceptoren beispielsweise sind ein spannendes Thema, weil hier Features wie die Transaktionalität der EJBs recht einfach nachgebaut werden kann. Trotzdem hoffe ich dass die Art, wie mit CDI in Java EE 6 gearbeitet werden kann, etwas sichtbar gemacht wurde. CDI ist eine attraktive weil leichtgewichtige Alternative zu EJB. Die duchgängige Unterstützung im schlanken Java EE 6 Web Profile macht diese Technologie für Web Applications besonders attraktiv.

Aber auch beim grossen Bruder EJB3.1 hat sich viel Spannendes getan! Mehr dazu bald an dieser Stelle.

Bis dahin beantworte ich gerne Rückfragen rund um CDI und/oder Java EE 6.

Hinterlasse eine Antwort

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind markiert *

*

*

Du kannst folgende HTML-Tags benutzen: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>