Überprüfung von Messages in einem logischen Request
 
  Änderung von DataBaseAwareConfigSource
 
  Window-ID in EL-Ausdrücken
 
  Window-ID bei HTML-Links
 
  View-Metadaten verändern
 
  Verzeichnishierarchie mit View-Configs
 
  Verwendung änderbarer View-Metadaten
 
  Verwendung von ViewConfigResolver
 
  Verwendung von preventDoubleSubmit
 
  Verwendung von Partial-Beans
 
  Verwendung von gruppierten Conversations
 
  Verwendung von GroupedConversationManager
 
  Verwendung von ExceptionToCatchEvent
 
  Verwendung von EntryPointNavigationEvent
 
  Verwendung von eigenen View-Metadaten
 
  Verwendung von BackNavigator
 
  Verwendung von @WindowScoped und PreViewConfigNavigateEvent
 
  Verwendung von @ViewMetaData
 
  Verwendung von @ViewControllerRef
 
  Verwendung von @ViewConfigRoot
 
  Verwendung von @ViewAccessScoped
 
  Verwendung von @PreRenderView
 
  Verwendung einer eigenen Scope-Annotation
 
  Utility Methoden von DeltaSpike
 
  User Registrierung und Login via Page-Bean-Test
 
  Umstellung auf Project-Stage abhängige Konfigurationswerte
 
  Typsichere Konfiguration ohne @ConfigProperty
 
  Typsichere JSF-Messages
 
  Typsichere Injizierung von Konfigurationswerten mit Default-Werten
 
  Typsichere Injizierung von Konfigurationswerten
 
  Transaktionale CDI-Beans statt EJBs
 
  Testen mit View-Controller Methoden
 
  Stereotype für transaktionale Services
 
  Stageabhängiges Startup-Event überwachen
 
  Stageabhängiges Startup-Event
 
  Seitenkonfigurationen für IdeaFork
 
  SecuredPages als Navigationsziel
 
  Restart des Request-Scopes in einer Test-Methode
 
  Quartz-Job als CDI-Bean
 
  Project-Stage abhängige Logik
 
  Producer für typsichere Konfigurationswerte
 
  Portabler Test-EntityManager-Producer
 
  PhaseListener bedingt aktivieren
 
  PhaseListener als CDI-Bean
 
  Partial-Bean ohne Producer
 
  Partial-Bean mit Producer
 
  Partial-Bean Binding
 
  Observer für ein portables Startup-Event
 
  Navigationsziel einschränken via Return-Typ
 
  Navigationsparameter via View-Config
 
  Möglichkeiten für Return-Typen
 
  MonitoringConfig als PartialBean
 
  MonitoringConfig als einfaches CDI-Beans
 
  Minimales Partial-Bean
 
  Minimaler Partial-Bean Handler
 
  Minimale View-Config in einem Verzeichnis
 
  Minimale View-Config
 
  Minimale typsichere JSF-Navigation
 
  Maven Konfiguration für DeltaSpike Weld-Control
 
  Maven Konfiguration für DeltaSpike Test-Control
 
  Maven Konfiguration für DeltaSpike Servlet
 
  Maven Konfiguration für DeltaSpike Security
 
  Maven Konfiguration für DeltaSpike Scheduler
 
  Maven Konfiguration für DeltaSpike Proxy
 
  Maven Konfiguration für DeltaSpike Partial-Bean
 
  Maven Konfiguration für DeltaSpike OWB-Control
 
  Maven Konfiguration für DeltaSpike OpenEJB/TomEE-Control
 
  Maven Konfiguration für DeltaSpike JSF (EE6)
 
  Maven Konfiguration für DeltaSpike JSF
 
  Maven Konfiguration für DeltaSpike JPA
 
  Maven Konfiguration für DeltaSpike Data
 
  Maven Konfiguration für DeltaSpike Core
 
  Maven Konfiguration für DeltaSpike CDI-Control
 
  Maven Konfiguration für DeltaSpike Bean-Validation
 
  Manueller Lookup
 
  Manuelle Verwendung von dependent-scoped Beans
 
  Manuelle Injizierung
 
  Manuelle Auswertungen
 
  Konfigurationswerte in TypedConfigHandler cachen
 
  Kombinierte Auswertung eigener Metadaten
 
  Klassen mit @Exclude für CDI exkludieren
 
  JPA-Entität für dynamische Konfigurationen
 
  Injizierung von Konfigurationen mit @ConfigProperty
 
  Injizierung via @DeltaSpike Qualifier
 
  Injizierung und Caching von konfigurierten Werten
 
  Implementierung von ConfigDescriptorValidator
 
  Globale Alternativen mit CDI 1.0
 
  Fenstermanagement mit WindowContext
 
  Fenstermanagement aktivieren
 
  Extension zur Validierung typsicherer Konfiguration
 
  Explizites Fenstermanagement
 
  Explizite Vergabe von Namen
 
  Explizite Gruppierung von Conversations
 
  Exception-Handler mit Navigation zu DefaultErrorView
 
  Erweiterung eines Stereotyps
 
  EntityManager-Producer unabhängig von EJBs
 
  Entity-Repository auf Basis von DeltaSpike-Data
 
  EL Integration für typsichere Nachrichten
 
  Einzelne Pfade verändern
 
  Eigener Project-Stage Wert
 
  Eigener Project-Stage in einem Exception-Handler
 
  Eigener Folder-NameBuilder
 
  Eigene Scope-Annotation
 
  Eigene Properties-Dateien einbinden
 
  Eigene Kontext-Implementierung registrieren
 
  Eigene Kontext-Implementierung
 
  Direkter Zugriff auf Services
 
  Definition typsicherer Nachrichten
 
  Default-Werte für typsichere Konfiguration
 
  Config-Source zum Laden von Werten aus einer Datenbank
 
  Config-Qualifier für typsichere Injizierung mit Default-Wert
 
  Config-Qualifier für typsichere Injizierung
 
  CDI-Bean als JMX-Bean aktivieren
 
  Beenden von gruppierten Conversations
 
  Bedingte Exkludierung von CDI-Beans mit @Exclude
 
  Auswertung eigener View-Metadaten
 
  Auswahl eines Eintrags
 
  Angepasste Namen kombinieren
 
  Aktivierung von TransactionalInterceptor für EE6-Server
 
  Aktivierung von Implementierungen nach Konfigurationsänderungen
 
  Aktivierung von CDIAwareConstraintValidatorFactory
 
  AccessDecisionVoter mit typsicheren Nachrichten
 
  Absicherung von Seiten mit @Secured

5 Apache DeltaSpike

In den vorherigen Kapiteln haben wir verschiedene CDI-Konzepte anhand der Beispielapplikation IdeaFork kennengelernt. Bevor wir im nächsten Kapitel IdeaFork auf ein effizientes Minimum reduzieren, bauen wir den aktuellen Stand von IdeaFork in diesem Kapitel weiter aus. Hierfür verwenden wir eine beliebte portable CDI-Erweiterung namens Apache DeltaSpike, welche Ende 2011 als Nachfolger von Apache MyFaces CODI und JBoss Seam3 gegründet wurde. Ein großer Teil aller CDI-basierten Applikationen setzte zum damaligen Zeitpunkt auf eine der beiden Erweiterungen, um von zusätzlichen Konzepten zu profitieren. CODI entstand aus Anforderungen großer Applikationen und zeichnete sich durch hohe Stabilität und gute Performance in Kombination mit innovativen Konzepten aus. Seam3 bot ähnlich wie CODI einige Module, welche CDI verbesserten und mit anderen Technologien integrierte. Obwohl CODI weniger Module zur Verfügung stellte, wurde das Framework in vielen CDI-basierten JSF-Applikationen bevorzugt, da das Framework die tägliche Arbeit vor allem mit CDI und JSF erheblich erleichterte. Knapp zwei Jahre nach der Gründung von CODI wurde der Community eine Fusion mit Seam3 angeboten, welche schließlich zu dem Top-Level-Projekt Apache DeltaSpike unter dem Dach der Apache Software Foundation (ASF) führte.

 

Der Anfang gestaltete sich etwas träge, da teilweise überlappende Aspekte zu einem konsistenten API vereint werden mussten. Mit Version 1.0 war es schließlich soweit und DeltaSpike deckte die erfolgreichsten Konzepte von CODI ab, wodurch die Migration einer CODI-basierten Applikation in vielen Fällen in wenigen Stunden durchführbar war. In fast allen Bereichen erfolgte jedoch eine komplette Neuimplementierung der bekannten Funktionalitäten, um diese noch komfortabler und gleichzeitig flexibler zur Verfügung zu stellen. Darüber hinaus wurden zusätzliche Mechanismen und Module hinzugefügt, die unter anderem durch Teile, die ursprünglich von Seam3 stammten, einfacher umgesetzt werden konnten. Nach einigen Diskussionen und Kompromissen wurden von Seam3 allerdings nur wenige Implementierungen übernommen, wodurch der Migrationsaufwand einer Seam3-Applikation, abhängig von den eingesetzten Teilen, umfangreicher ausfallen kann. Einige der Seam3 Module wurden in Projekte von Drittherstellern verschoben. Bei diesen Modulen handelt es sich primär um die CDI-Integration der entsprechenden Projekte, welche in Zukunft von den Projekten selbst weiterentwickelt und gewartet wird. DeltaSpike selbst sorgt somit primär für die direkte Verbesserung von CDI und anderer Java EE Spezifikationen und ermöglicht darüber hinaus die portable Verwendung von CDI 1.x in Java SE Projekten. Die praxisnahe Erweiterung von Java EE, aber auch von CDI selbst, ist das Erfolgsrezept von DeltaSpike. Dies verhalf dem Projekt zu einer vielfältigen Community und 2014 sogar zu einem "Duke's Choice Award".

 

Ein weiterer Erfolgsfaktor ist die getestete Portabilität, welche seit Beginn des Projekts eine zentrale Rolle spielt. Mit Hilfe von JBoss Arquillian wurde ein umfangreiches Set an Tests aufgebaut. Für Releases von OpenWebBeans und Weld wird jeweils ein eigener Build-Job im Continuous-Integration Cluster der Apache Software Foundation angelegt. Dies ermöglicht automatisierte Tests, welche die Kompatibilität von DeltaSpike mit möglichst vielen Konfigurationen sicherstellt. Neben OpenWebBeans und Weld selbst wird die Test-Suite auch in Kombination mit mehreren Open-Source Servern regelmäßig ausgeführt. Hierzu zählen verschiedene Versionen von Apache TomEE, JBoss AS7 und WildFly, sowie Oracle GlassFish.

 

Mittlerweile ist DeltaSpike so umfangreich, dass die Beschreibung sämtlicher Bestandteile den Rahmen dieses Buchs übersteigen würde. In den nachfolgenden Teilen werden wir uns daher die zentralsten Bestandteile und ein paar der Erweiterungsmöglichkeiten ansehen. Hierfür gehen wir von unserer CDI-basierten JSF Beispielsapplikation namens IdeaFork aus und stellen Teile dieser um.

5.1 Alle für einen Kern

DeltaSpike besteht aus mehreren größtenteils unabhängigen Modulen, welche auf Basis von DeltaSpike-Core aufgebaut sind. Die Verwendung einzelner Module ist optional. Soll eine CDI-basierte Applikation mit DeltaSpike verbessert werden, dann muss mindestens DeltaSpike-Core hinzugefügt werden. Neben verschiedenen Utilities für CDI enthält DeltaSpike-Core zusätzliche Funktionalitäten, die sowohl in CDI-basierten Java SE als auch Java EE Applikationen nützlich sind.

 

Bevor wir IdeaFork mit Mechanismen von DeltaSpike bereichern, müssen wir DeltaSpike-Core als Dependency von IdeaFork hinzufügen. Im Normalfall genügt es Core-API als compile -Dependency und Core-Impl als runtime -Dependency zu definieren. Beides ist in Listing Maven Konfiguration für DeltaSpike Core ersichtlich und wird hier mit der separat definierten Maven-Property ${ds.version} parametrisiert.
<dependency>
  <groupId>org.apache.deltaspike.core</groupId>
  <artifactId>deltaspike-core-api</artifactId>
  <version>${ds.version}</version>
</dependency>
<dependency>
  <groupId>org.apache.deltaspike.core</groupId>
  <artifactId>deltaspike-core-impl</artifactId>
  <version>${ds.version}</version>
  <scope>runtime</scope>
</dependency>
Neben DeltaSpike-Core werden wir in diesem Buch die folgenden Module auszugsweise betrachten:
Die DeltaSpike-Module Security, Partial-Bean und Proxy sind teilweise für Funktionalitäten der zuvor aufgelisteten Module erforderlich. Alle drei Module können auch eigenständig eingesetzt werden. Allerdings werden wir uns primär ihren Einsatz in Kombination mit anderen Bestandteilen von DeltaSpike ansehen.

 

Mit Hilfe von DeltaSpike-Core und den verschiedenen Modulen werden wir IdeaFork Schritt für Schritt verbessern. Da einige Mechanismen primär in Kombination mit anderen sinnvoll veranschaulicht werden können, werden wir nicht jedes Modul isoliert betrachten, sondern IdeaFork thematisch umstellen indem verschiedene Teile von DeltaSpike kombiniert werden. Bevor wir mit der Umstellung von IdeaFork beginnen, sehen wir uns die verfügbaren Module und deren Maven-Konfiguration im Überblick an.

5.1.0.1 DeltaSpike Bean-Validation

In Java EE6 ist eine rudimentäre Integration zwischen Bean-Validation und CDI definiert. Vor allem durch CDI verwaltete Constraint-Validatoren werden nicht unterstützt. Diese Einschränkung wurde mit Java EE7 behoben. In IdeaFork haben wir dies manuell in BeanAwareConstraintValidatorFactory umgesetzt. Eine solche Integration wird auch durch das in Listing Maven Konfiguration für DeltaSpike Bean-Validation gezeigte Modul zur Verfügung gestellt.
<dependency>
  <groupId>org.apache.deltaspike.modules</groupId>
  <artifactId>deltaspike-bean-validation-module-api</artifactId>
  <version>${deltaspike.version}</version>
</dependency>
<dependency>
  <groupId>org.apache.deltaspike.modules</groupId>
  <artifactId>deltaspike-bean-validation-module-impl</artifactId>
  <version>${deltaspike.version}</version>
  <scope>runtime</scope>
</dependency>
Tipp: Alle DeltaSpike Module bestehen aus einem API- und einem Impl-Teil. Das BeanValidation-Modul hält sich an diese Konvention, um keine Verwirrung zu verursachen. Allerdings ist derzeit kein API erforderlich und somit würde die Impl-Dependency ausreichen.

5.1.0.2 DeltaSpike Data

Das Data-Modul erlaubt eine effizientere Verwendung einfacher JPQL (Java Persistence Query Language) Queries. Im Hintergrund wird die entsprechende JPQL-Query auf Basis der Methodensignatur bzw. optional auf Basis zusätzlicher Metadaten generiert.

 

Listing Maven Konfiguration für DeltaSpike Data zeigt die Einträge für die Maven Konfiguration. Als transitive Dependencies bindet DeltaSpike-Data das JPA-, Partial-Bean- und Proxy-Modul ein.
<dependency>
  <groupId>org.apache.deltaspike.modules</groupId>
  <artifactId>deltaspike-data-module-api</artifactId>
  <version>${deltaspike.version}</version>
</dependency>
<dependency>
  <groupId>org.apache.deltaspike.modules</groupId>
  <artifactId>deltaspike-data-module-impl</artifactId>
  <version>${deltaspike.version}</version>
  <scope>runtime</scope>
</dependency>

5.1.0.3 DeltaSpike JPA

Falls in einer CDI-basierten Anwendung EJBs nicht zur Wahl stehen oder reine CDI-Beans bevorzugt werden, dann bietet dieses Modul ein alternatives Transaction-Handling. Neben verschiedenen Strategien zur Behandlung von Transaktionen, die in Java SE und EE Applikationen verwendet werden können, definiert dieses Modul bspw. einen CDI-Kontext der die Lebensdauer entsprechender CDI-Beans auf die aktuelle Transaktion beschränkt.

 

Die in Listing Maven Konfiguration für DeltaSpike JPA gezeigten Einträge müssen angegeben werden, wenn dieses Modul nicht in Kombination mit DeltaSpike Data verwendet werden soll.
<dependency>
  <groupId>org.apache.deltaspike.modules</groupId>
  <artifactId>deltaspike-jpa-module-api</artifactId>
  <version>${deltaspike.version}</version>
</dependency>
<dependency>
  <groupId>org.apache.deltaspike.modules</groupId>
  <artifactId>deltaspike-jpa-module-impl</artifactId>
  <version>${deltaspike.version}</version>
  <scope>runtime</scope>
</dependency>

5.1.0.4 DeltaSpike JSF

Die Build-Dependencies aus Listing Maven Konfiguration für DeltaSpike JSF (EE6) sind erforderlich, wenn eine JSF-Applikation noch effizienter entwickelt werden soll. Einige Konzepte dieses Moduls ermöglichen eine höhere Typsicherheit, wodurch der Wartungsaufwand gesenkt werden kann. Die Integration mit anderen Teilen von DeltaSpike erlaubt zusätzlich die einheitliche Verwendung verschiedener Mechanismen des Frameworks. Hierfür ist neben dem Proxy-Modul vor allem das Security-Modul als transitive Dependency erforderlich.
<dependency>
  <groupId>org.apache.deltaspike.modules</groupId>
  <artifactId>deltaspike-jsf-module-api</artifactId>
  <version>${deltaspike.version}</version>
</dependency>
<dependency>
  <groupId>org.apache.deltaspike.modules</groupId>
  <artifactId>deltaspike-jsf-module-impl-ee6</artifactId>
  <version>${deltaspike.version}</version>
  <scope>runtime</scope>
</dependency>
Das normale JSF-Modul ist mit EE6 und EE7 kompatibel. Einige EE6 Server loggen jedoch beim Applikationsstart einen Fehler. Der Grund hierfür ist die Deaktivierung optionaler Klassen des JSF-Moduls, welche für den EE7-Support nötig sind. Die Applikation ist dennoch funktionsfähig. Um eine mögliche Verunsicherung zu vermeiden, kann auf ein EE6 spezifisches Modul zurückgegriffen werden. Daher ist es für EE6 basierte Applikationen ebenfalls möglich die Build-Dependencies aus Listing Maven Konfiguration für DeltaSpike JSF zu verwenden. Durch diesen Ansatz ändert sich der Funktionsumfang nicht und der Wartungsaufwand der Module kann minimiert werden.
<dependency>
  <groupId>org.apache.deltaspike.modules</groupId>
  <artifactId>deltaspike-jsf-module-api</artifactId>
  <version>${ds.version}</version>
</dependency>
<dependency>
  <groupId>org.apache.deltaspike.modules</groupId>
  <artifactId>deltaspike-jsf-module-impl</artifactId>
  <version>${ds.version}</version>
  <scope>runtime</scope>
</dependency>

5.1.0.5 DeltaSpike Partial-Bean

Partial-Beans sind Interfaces- oder abstrakte Klassen für welche eine generische Behandlung zur Verfügung gestellt werden kann. Die in Listing Maven Konfiguration für DeltaSpike Partial-Bean aufgelisteten Build-Dependencies ermöglichen eine solche Entkoppelung durch spezielle Bindings.

 

Dieses Konzept ist unter anderem die Basis für das Data-Modul. Die Generierung von JPQL-Queries ist generisch im Data-Modul implementiert, wodurch die Applikation hierfür keine Logik enthalten muss. JPA-Repositories einer Applikation können dadurch auf Interfaces- oder abstrakte Klassen reduziert werden.
<dependency>
  <groupId>org.apache.deltaspike.modules</groupId>
  <artifactId>deltaspike-partial-bean-module-api</artifactId>
  <version>${ds.version}</version>
</dependency>
<dependency>
  <groupId>org.apache.deltaspike.modules</groupId>
  <artifactId>deltaspike-partial-bean-module-impl</artifactId>
  <version>${ds.version}</version>
  <scope>runtime</scope>
</dependency>
Für die Funktionalität dieses Moduls sind Proxies erforderlich, wodurch das Proxy-Modul von DeltaSpike als transitive Dependency definiert ist.

5.1.0.6 DeltaSpike Proxy

Das Proxy-Modul entkoppelt die Verwendung von Proxy-Funktionalitäten von einer konkreten Implementierung und wird, wie in Listing Maven Konfiguration für DeltaSpike Proxy zu sehen ist, etwas anders konfiguriert. Derzeit ist das ASM5-Modul die einzige Implementierung. In Zukunft kann es hier weitere Implementierungen geben, um bspw. neue JDK-Versionen zu unterstützen.
<dependency>
  <groupId>org.apache.deltaspike.modules</groupId>
  <artifactId>deltaspike-proxy-module-api</artifactId>
  <version>${ds.version}</version>
</dependency>
<dependency>
  <groupId>org.apache.deltaspike.modules</groupId>
  <artifactId>deltaspike-proxy-module-impl-asm5</artifactId>
  <version>${ds.version}</version>
  <scope>runtime</scope>
</dependency>

5.1.0.7 DeltaSpike Scheduler

Dieses Modul erlaubt die Integration mit Schedulern, die eine Task-/Job-Konfiguration mit Cron-Ausdrücken unterstützen. Da zusätzlich DeltaSpike CDI-Control benötigt wird, um je Task/Job bspw. den Request-Context zu starten und am Ende wieder zu stoppen, ist dieses Modul nicht mit allen EE Servern kompatibel. Moderne EE-Server, die aktuelle Versionen von OpenWebBeans oder Weld einsetzen, sind von dieser Einschränkung in der Regel nicht betroffen.

 

Build-Dependencies für die Integration mit Quartz sind in Listing Maven Konfiguration für DeltaSpike Scheduler ersichtlich.
<dependency>
  <groupId>org.apache.deltaspike.modules</groupId>
  <artifactId>deltaspike-scheduler-module-api</artifactId>
  <version>${ds.version}</version>
</dependency>
<dependency>
  <groupId>org.apache.deltaspike.modules</groupId>
  <artifactId>deltaspike-scheduler-module-impl</artifactId>
  <version>${ds.version}</version>
  <scope>runtime</scope>
</dependency>

<dependency>
  <groupId>org.quartz-scheduler</groupId>
  <artifactId>quartz</artifactId>
  <version>${quartz.version}</version>
</dependency>

5.1.0.8 DeltaSpike Security

Bei dem Security-Modul aus Listing Maven Konfiguration für DeltaSpike Security handelt es sich nicht um ein vollständiges Security-Framework. Stattdessen ermöglicht dieses Modul bestehende Security-Framework mit CDI-Beans auf einfache Weise zu integrieren. In Kombination mit dem JSF-Modul können zusätzlich JSF-Seiten mit den gleichen Konzepten abgesichert werden.
<dependency>
  <groupId>org.apache.deltaspike.modules</groupId>
  <artifactId>deltaspike-security-module-api</artifactId>
  <version>${ds.version}</version>
</dependency>
<dependency>
  <groupId>org.apache.deltaspike.modules</groupId>
  <artifactId>deltaspike-security-module-impl</artifactId>
  <version>${ds.version}</version>
  <scope>runtime</scope>
</dependency>

5.1.0.9 DeltaSpike Servlet

Ähnlich wie bei Bean-Validation wurde eine vollständige CDI-Integration für Servlets erst in Java EE7 umgesetzt. Die in Listing Maven Konfiguration für DeltaSpike Servlet enthaltenen Dependencies stellen primär diese Funktionalitäten auch für Java EE6 basierte Applikationen zur Verfügung.
<dependency>
  <groupId>org.apache.deltaspike.modules</groupId>
  <artifactId>deltaspike-servlet-module-api</artifactId>
  <version>${ds.version}</version>
</dependency>
<dependency>
  <groupId>org.apache.deltaspike.modules</groupId>
  <artifactId>deltaspike-servlet-module-impl</artifactId>
  <version>${ds.version}</version>
  <scope>runtime</scope>
</dependency>

5.1.0.10 DeltaSpike Test-Control

Diese CDI-Integration für JUnit verwenden wir für die Beispiele dieses Buchs seit dem ersten Commit im Git-Repository von IdeaFork . Neben DeltaSpike-Core ist auch DeltaSpike CDI-Control erforderlich, damit die Test-Dependencies aus Listing Maven Konfiguration für DeltaSpike Test-Control für einfache Tests von CDI-basierten Applikationen genutzt werden können.
<dependency>
  <groupId>org.apache.deltaspike.modules</groupId>
  <artifactId>deltaspike-test-control-module-api</artifactId>
  <version>${ds.version}</version>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>org.apache.deltaspike.modules</groupId>
  <artifactId>deltaspike-test-control-module-impl</artifactId>
  <version>${ds.version}</version>
  <scope>test</scope>
</dependency>

5.1.0.11 DeltaSpike CDI-Control

DeltaSpike CDI-Control ist parallel zu DeltaSpike-Core zu sehen, da dieser Teil von DeltaSpike nicht auf DeltaSpike-Core aufbaut, aber die Basis für DeltaSpike-Scheduler und DeltaSpike-Test-Control ist.

 

Ursprünglich für Java SE konzipiert, funktioniert CDI-Control auch mit modernen Java EE Servern. Hierfür stellt DeltaSpike Implementierungen für OpenWebBeans, OpenEJB und Weld bereit. Durch diesen Ansatz kann in einer CDI-basierten Applikation auf die direkte Verwendung proprietärer Container-APIs verzichtet werden. CDI-Control verbirgt diese Aufrufe hinter einem einheitlichen API. Der einzige Unterschied zur Laufzeit ist das jeweils eingebundene Implementierungsmodul. Gleiches gilt auch für die manuelle Kontrolle der Standard-Scopes von CDI. Implementierungen des Interfaces ContextControl können die dahinter liegenden Kontexte über proprietäre Container-APIs starten und stoppen.

 

Abhängig vom Verwendungsziel kann das API-Modul aus Listing Maven Konfiguration für DeltaSpike CDI-Control als compile oder test -Dependency eingebunden werden.
<dependency>
  <groupId>org.apache.deltaspike.cdictrl</groupId>
  <artifactId>deltaspike-cdictrl-api</artifactId>
  <version>${ds.version}</version>
  <scope>...</scope>
</dependency>
Per Definition gibt es für das API-Modul von CDI-Control mehrere Implementierungsmodule, welche in den Listings Maven Konfiguration für DeltaSpike OWB-Control , Maven Konfiguration für DeltaSpike Weld-Control und Maven Konfiguration für DeltaSpike OpenEJB/TomEE-Control ersichtlich sind.
<dependency>
  <groupId>org.apache.deltaspike.cdictrl</groupId>
  <artifactId>deltaspike-cdictrl-owb</artifactId>
  <version>${ds.version}</version>
  <scope>...</scope>
</dependency>
<dependency>
  <groupId>org.apache.deltaspike.cdictrl</groupId>
  <artifactId>deltaspike-cdictrl-weld</artifactId>
  <version>${ds.version}</version>
  <scope>...</scope>
</dependency>
<dependency>
  <groupId>org.apache.deltaspike.cdictrl</groupId>
  <artifactId>deltaspike-cdictrl-openejb</artifactId>
  <version>${ds.version}</version>
  <scope>...</scope>
</dependency>

5.2 Flexible Spielregeln

In einem ersten Schritt beginnen wir mit der Verwendung von @Exclude , um die bisher verwendete CDI-Extension namens EntityVetoExtension zu ersetzen. Das Ziel von EntityVetoExtension ist ein Veto auf alle JPA-Entitäten durchzuführen, damit diese nicht als CDI-Beans zur Verfügung stehen. In IdeaFork können wir das gleiche Ergebnis erzielen, indem wir die Klasse BaseEntity mit @Exclude annotieren. Für IdeaFork funktioniert dies, da alle JPA-Entitäten von dieser Basisklasse ableiten. Dennoch sind beide Lösungen nicht vollständig äquivalent, da wir jetzt JPA-Entitäten als CDI-Beans zulassen sobald diese nicht von BaseEntity ableiten und nicht explizit mit @Exclude markiert sind. In IdeaFork erhalten wir jedoch das gleiche Ergebnis und können dafür auf eine eigene Erweiterung verzichten. Gleichzeitig erhöhen wir durch die explizite Angabe von @Exclude die Lesbarkeit der Applikation. Listing Klassen mit @Exclude für CDI exkludieren zeigt die eben beschriebene simple Verwendung von @Exclude . Abgesehen von dieser Ergänzung wird in IdeaFork die Klasse EntityVetoExtension gelöscht und der dazugehörige Konfigurationseintrag entfernt.
@Exclude
@MappedSuperclass
public abstract class BaseEntity implements Serializable {
  //...
}
Tipp: Für CDI selbst ist DeltaSpike eine Art Ideenpool. So wurde bspw. ein Teil von @Exclude in CDI 1.1 übernommen und steht seit dieser Version unter dem Namen @Vetoed zur Verfügung. @Exclude ist jedoch weiterhin sinnvoll, da diese Annotation die Verwendung zusätzlicher Bedingungen für die Deaktivierung von CDI-Beans unterstützt.
Zur Deaktivierung von CDI-Beans können auch Bedingungen verwendet werden. Ein Beispiel hierfür sind die Project-Stages, die ebenfalls von DeltaSpike zur Verfügung gestellt werden. In Java EE steht das Konzept der Project-Stages nur für JSF zur Verfügung. DeltaSpike greift die Grundidee auf und stellt diese mit einem typsicheren und gleichzeitig erweiterbaren Mechanismus für alle Teile einer Applikation zur Verfügung. Jeder Project-Stage bildet einen anderen Schritt bei der Applikationsentwicklung ab. Sollten die vordefinierten Stages UnitTest , Development , SystemTest , IntegrationTest , Staging und Production für eine Applikation nicht genügen, dann können eigene Typen registriert werden. Bei der Verwendung von Stages gibt es keinen Unterschied zwischen den vordefinierten und selbst definierten Stages.

 

Die Kombination von Project-Stages mit der Annotation @Exclude ermöglicht die Deaktivierung von CDI-Beans in bestimmten Stages. Listing Bedingte Exkludierung von CDI-Beans mit @Exclude zeigt die einfachste Variante einer solchen Kombination. In diesem Fall deaktivieren wir IdeaSavedObserver für alle Stages außer Development und UnitTest .
@ApplicationScoped
@Exclude(exceptIfProjectStage =
  {ProjectStage.Development.class, ProjectStage.UnitTest.class})
public class IdeaSavedObserver {
  //...
}
Wird dieses Konzept zusätzlich bspw. mit CDI-Beans kombiniert die mit @Alternative annotiert sind, so können alternative Implementierungen für verschiedene Stages aktiviert werden ohne spezielle Versionen der Applikation zu erstellen. In IdeaFork werden wir etwas später ein Mail-Service hinzufügen, für welches wir eine gemockte alternative Implementierung umsetzen. Zusätzlich könnten wir diese alternative Implementierung mit @Exclude annotieren, um diese gemockte Implementierung bspw. nur während der Entwicklung und für Unit-Tests zu verwenden. Wird das alternative CDI-Bean später bei Project-Stage Production durch eine solche Bedingung deaktiviert, dann wird automatisch das ursprüngliche CDI-Bean aktiv.
Tipp: Das CDI-TestControl-Modul aktiviert standardmäßig den Project-Stage UnitTest . Dieses Default-Verhalten kann mit der optionalen Annotation @TestControl explizit je Test-Methode oder Test-Klasse übersteuert werden.
Die Konfiguration des aktuellen Project-Stages kann über den Konfigurationsmechanismus von DeltaSpike durchgeführt werden. Hierfür muss der entsprechende Name eines Stages für den Key org.apache.deltaspike.ProjectStage aktiviert werden. Zusätzlich ist eine rudimentäre Integration mit JSF-Project-Stages verfügbar, sofern der JSF-Project-Stage via JNDI und einem der Standardkeys ( javax.faces.PROJECT_STAGE oder faces.PROJECT_STAGE ) konfiguriert ist.
Tipp: Wird der JSF-Project-Stage mit einem Eintrag in der Datei web.xml konfiguriert, dann wird dieser von DeltaSpike bewusst ignoriert, weil diese Konfigurationsvariante oft zu Problemen in der Praxis geführt hat. Hin und wieder kommt es vor, dass ein solcher Eintrag vergessen bzw. versehentlich geändert wurde und Applikationen dadurch nicht mit Project-Stage Production auf einem produktiven System deployed wurden. Da der Konfigurationsmechanismus von DeltaSpike erweiterbar ist, kann bei Bedarf diese bewusst gewählte Einschränkung mit Hilfe einer eigenen Implementierung von org.apache.deltaspike.core.spi.config.ConfigSource umgangen werden.

 

Der Konfigurationsmechanismus von DeltaSpike ist sehr vielfältig. In Kombination mit dem Qualifier @ConfigProperty können konfigurierte Werte in CDI-Beans injiziert werden. Ein einfaches Beispiel wird in Listing Injizierung von Konfigurationen mit @ConfigProperty illustriert.

 

Ursprünglich haben wir in CurrentObjectConverterProducer unsere eigene typsichere Konfiguration verwendet. In einfachen Fällen ist dies durchaus eine elegante Möglichkeit. Allerdings mussten wir hierfür den Wert manuell laden. Um dies zu vermeiden können wir stattdessen @ConfigProperty verwenden.
@ApplicationScoped
public class CurrentObjectConverterProducer {
  @Produces
  @Default
  @Dependent
  protected ObjectConverter defaultConverter(
      @ExternalFormat(XML) ObjectConverter objectConverterXml,
      @ExternalFormat(JSON) ObjectConverter objectConverterJson,
      @ConfigProperty(name = "defaultExternalFormat")
        String defaultExternalFormat) {
    switch (ExternalFormat.TargetFormat.valueOf(defaultExternalFormat)) {
      case JSON:
        return objectConverterJson;
      default:
        return objectConverterXml;
    }
  }
}
Standardmäßig wertet DeltaSpike verschiedene Konfigurationsquellen aus. System-Properties werden vor Umgebungsvariablen und vor einem JNDI-Lookup abgefragt. Als letzte Quelle lädt DeltaSpike alle Konfigurationen mit dem Namen META-INF/apache-deltaspike.properties . Werte aus einer Config-Source mit höherer Priorität übersteuern Werte aus nachgelagerten Quellen.
Tipp: Die vordefinierte Reihenfolge kann angepasst werden, da die Priorität einer Config-Source verändert werden kann. Soll bspw. JNDI die höchste Priorität haben, so muss der Key deltaspike_ordinal mit Hilfe der Konfigurationsquelle selbst, in diesem Fall als JNDI-Eintrag, mit dem höchsten Ordinal-Wert der aktivierten Konfigurationsquellen gesetzt werden. Konkret müsste bspw. deltaspike_ordinal=500 via JNDI-Konfiguration festgelegt werden.

 

Normalerweise wollen wir für die Konfiguration einer Applikation eine eigene Konfigurationsdatei verwenden. So sind auch in IdeaFork konfigurierte Werte in einer eigenen Datei namens app-config.properties abgelegt. Genau genommen kennt DeltaSpike nur das abstrakte Konzept von Konfigurationsquellen und liefert Implementierungen für Quellen wie bspw. META-INF/apache-deltaspike.properties mit. Die Erweiterbarkeit des Konfigurationsmechanismuses ermöglicht die Integration anderer Konfigurationsquellen durch Implementierungen des Interfaces org.apache.deltaspike.core.spi.config.ConfigSource .

 

Für eigene Property-Dateien stellt DeltaSpike eine noch einfachere Integration zur Verfügung. Listing Eigene Properties-Dateien einbinden zeigt die Verwendung der Basisklasse PropertyFileConfig . Neben dem Namen selbst muss explizit angegeben werden, ob es sich um eine optionale Konfigurationsdatei handelt. DeltaSpike sucht während des Applikationsstarts Implementierungen von org.apache.deltaspike.core.api.config.PropertyFileConfig und registriert diese automatisch in der Bootstrapping-Phase AfterDeploymentValidation . Daher stehen so konfigurierte Werte erst am Ende des Containerstarts zur Verfügung.
Tipp: Soll ein konfigurierter Wert bereits während der Bootstrapping-Phase verfügbar sein, dann ist eine Implementierung des Interfaces org.apache.deltaspike.core.spi.config.ConfigSource erforderlich. Die Aktivierung einer Implementierung dieses Interfaces folgt den standard Service-Loader-Regeln und ist somit unabhängig von CDI.
public class IdeaForkConfigFile implements PropertyFileConfig {
  @Override
  public String getPropertyFileName() {
    return "app-config.properties";
  }

  @Override
  public boolean isOptional() {
    return false;
  }
}
IdeaForkConfigFile ermöglicht uns Werte aus der Konfigurationsdatei app-config.properties ebenfalls mit Hilfe der Qualifier-Annotation @ConfigProperty zu injizieren. Werte die auf diese Weise injiziert werden haben keinen eigenen Lifecycle. In vielen Fällen handelt es sich um Strings und primitive Datentypen, wodurch DeltaSpike selbst keinen automatischen Reload-Mechanismus für solche Werte bereitstellen kann. Hier können wir allerdings auf Boardmittel von CDI zurückgreifen. In Listing Injizierung und Caching von konfigurierten Werten wird ein Request-scoped Bean verwendet, um den konfigurierten Wert einmal je Request einzulesen. Dies ist natürlich ein gewisser Overhead, den wir im nächsten Abschnitt durch einen eigenen Scope minimieren werden.
@RequestScoped
public class MonitoringConfig {
  @Inject
  @ConfigProperty(name = "methodInvocationThreshold")
  private Integer methodInvocationThreshold;

  public Integer getMethodInvocationThreshold() {
    return methodInvocationThreshold;
  }
}
Das CDI-Bean aus Listing Injizierung und Caching von konfigurierten Werten kann anschließend an beliebigen Stellen injiziert und verwendet werden, um auf die aktuellen Werte zuzugreifen.

 

Soll hingegen ein konfigurierter Wert direkt an mehreren Stellen in der Applikation injiziert werden, so ist es möglich einen eigenen Qualifier zu erstellen, um die Typsicherheit zu erhöhen und den String für den Konfigurationskey an einer zentralen Stelle zu kapseln. Listing Config-Qualifier für typsichere Injizierung zeigt einen solchen Qualifier. Abgesehen von den Annotationen für CDI-Qualifier wird ein solcher Qualifier mit @ConfigProperty annotiert. Dadurch ist in diesem Beispiel @ConfigProperty(name = "name") zentral in der Annotation @ApplicationName gekapselt.
@ConfigProperty(name = "name")
@Target({METHOD, FIELD, PARAMETER})
@Retention(RUNTIME)
@Qualifier
public @interface ApplicationName {
}
Listing Typsichere Injizierung von Konfigurationswerten zeigt den dazu passenden Injection-Point, bei welchem statt @ConfigProperty der Qualifier aus Listing Config-Qualifier für typsichere Injizierung verwendet wird. Da wir beim Injection-Point einen eigenen Qualifier verwenden, müssen wir auch einen entsprechenden Producer zur Verfügung stellen.
@Inject
@ApplicationName
private String applicationName;
Listing Producer für typsichere Konfigurationswerte veranschaulicht die erforderliche Producer-Implementierung, welche durch die Verwendung von org.apache.deltaspike.core.spi.config.BaseConfigPropertyProducer sehr einfach gehalten werden kann. Im konkreten Beispiel muss nur an BaseConfigPropertyProducer#getStringPropertyValue delegiert werden.
@ApplicationScoped
public class ConfigProducer extends BaseConfigPropertyProducer {
  @Produces
  @ApplicationName
  public String applicationName(InjectionPoint injectionPoint) {
    return getStringPropertyValue(injectionPoint);
  }

  //...
}
Natürlich ist eine solche simple Delegation nicht in jedem Fall möglich. Listing Default-Werte für typsichere Konfiguration zeigt bspw. eine weitere Producer-Methode in der gleichen Klasse, welche das geladene Ergebnis anschließend aufbereitet. Der hierfür erforderliche Qualifier ist in Listing Config-Qualifier für typsichere Injizierung mit Default-Wert angegeben. Das Annotation-Attribut defaultValue wird in der Methode ConfigProducer#maxNumberOfHighestRatedCategories manuell ausgewertet und muss daher mit @Nonbinding markiert werden.
@ConfigProperty(name = "maxNumberOfHighestRatedCategories")
@Target({METHOD, PARAMETER, FIELD})
@Retention(RUNTIME)
@Qualifier
public @interface MaxNumberOfHighestRatedCategories {
  @Nonbinding
  int defaultValue() default 15;
}
Außerdem wird in Listing Default-Werte für typsichere Konfiguration gezeigt, dass eigene Qualifier auch eine zusätzliche Möglichkeit bieten indem eigene Annotation-Attribute verwendet werden können.
@ApplicationScoped
public class ConfigProducer extends BaseConfigPropertyProducer {
  //...

  @Produces
  @MaxNumberOfHighestRatedCategories
  public Integer maxNumberOfHighestRatedCategories(
      InjectionPoint injectionPoint) {

    String configuredValue = getStringPropertyValue(injectionPoint);

    if (configuredValue == null || configuredValue.length() == 0) {
      return getAnnotation(
        injectionPoint, MaxNumberOfHighestRatedCategories.class)
        .defaultValue();
    }

    return Integer.parseInt(configuredValue);
  }
}
Tipp: Da wir Informationen vom Injection-Point auswerten müssen, können wir in den vorherigen Beispielen lt. den CDI-Regeln nur dependent-scoped Beans erzeugen.

 

Bei Injection-Points mit dem Qualifier @MaxNumberOfHighestRatedCategories kann notfalls sogar der Default-Wert verändert werden. Normalerweise ist dies jedoch nicht erforderlich, wodurch die in Listing Typsichere Injizierung von Konfigurationswerten mit Default-Werten gezeigte Verwendung des Qualifiers bei einem Injection-Point in der Regel ausreichend ist.
@Repository
public class IdeaJpaRepository
  extends GenericJpaRepository<Idea>
  implements IdeaRepository {

    @Inject
    @MaxNumberOfHighestRatedCategories
    private Integer maxNumberOfHighestRatedCategories;

    //...
}
Im Hintergrund delegiert BaseConfigPropertyProducer an die Klasse ConfigResolver von DeltaSpike, welche natürlich auch manuell verwendet werden kann. Listing Typsichere Konfiguration ohne @ConfigProperty zeigt eine herkömmliche Producer-Methode ohne @ConfigProperty und ohne Analyse des Injection-Points. Stattdessen wird ConfigResolver#getPropertyValue in Kombination mit einem fixen Key verwendet und das geladene Ergebnis wird durch die Klasse ApplicationVersion als strukturiertes Objekt zur Verfügung gestellt.
@ApplicationScoped
public class ConfigProducer extends BaseConfigPropertyProducer {
  //...

  @Produces
  @Dependent
  public ApplicationVersion applicationVersion() {
    String configuredValue = ConfigResolver.getPropertyValue("version");
    return new ApplicationVersion(configuredValue);
  }
}

public class ApplicationVersion {
  private final boolean released;
  private final String versionString;

  public ApplicationVersion(String versionString) {
    this.released = !versionString.contains("SNAPSHOT");
    this.versionString = versionString;
  }

  public boolean isReleased() {
    return released;
  }

  @Override
  public String toString() {
    return versionString;
  }
}
ApplicationVersion aus Listing Typsichere Konfiguration ohne @ConfigProperty kann somit wie gewohnt in andere Beans injiziert werden. Listing Project-Stage abhängige Logik verarbeitet die Information aus ApplicationVersion falls ein bestimmter Project-Stage aktiv ist. Die im vorherigen Abschnitt vorgestellten Project-Stages können nämlich auch manuell ausgewertet werden. Der aktive Project-Stage kann injiziert und mit == oder #equals überprüft werden.
@Named
public class ApplicationInfo {
  private String versionText = "Public";

  @Inject
  public ApplicationInfo(ApplicationVersion appVersion,
                         ProjectStage projectStage) {

    if (projectStage == Staging) {
      if (appVersion.isReleased()) {
        versionText = "Release ";
      }
      versionText += "v" + appVersion.toString();
    }
  }

  public String getVersionText() {
    return versionText;
  }
}
Bisher haben wir die Versionsnummer von IdeaFork in unserer Konfigurationsdatei fix abgelegt und nur zur Veranschaulichung typsicherer Konfigurationsklassen verwendet. Um Project-Stage basierte Logik in Kombination mit Methoden wie bspw. #isReleased sinnvoller zu verwenden, ist es allerdings naheliegend die Versionsnummer aus der Build-Konfiguration zu verwenden. Wie bei jeder Konfigurationsdatei in einem Projekt mit Maven-Build kann hierfür der Platzhalter ${project.version} verwendet werden. Dadurch wird es in Listing Project-Stage abhängige Logik möglich bei Project-Stage Staging die exakte Build-Version anzuzeigen ohne diese manuell zu warten.

 

Im XHTML-Template von IdeaFork kann daher eine einfache EL-Expression, in unserem Fall #{applicationInfo.versionText} , verwendet werden, um abhängig vom aktuellen Project-Stage verschiedene Informationen anzuzeigen.

 

Wie bereits im vorherigen Abschnitt erwähnt kann der aktuelle Project-Stage mit dem Key org.apache.deltaspike.ProjectStage konfiguriert werden. Da VM-Parameter eine der standardmäßig unterstützen Konfigurationsquellen ist, können wir durch -Dorg.apache.deltaspike.ProjectStage=Staging die Version von IdeaFork anzeigen lassen.

 

Der Project-Stage Mechanismus von DeltaSpike ist nicht nur typsicher, sondern auch erweiterbar. Um einen zusätzlichen Stage zu definieren muss das Interface org.apache.deltaspike.core.api.projectstage.ProjectStageHolder implementiert werden. Anschließend müssen wir diese Klasse in der Datei META-INF/services/org.apache.deltaspike.core.api.projectstage.ProjectStageHolder nach den herkömmlichen Service-Loader Regeln konfigurieren. Wie in Listing Eigener Project-Stage Wert ersichtlich ist, muss die Implementierung eine (public static final) Variable initialisiert zur Verfügung stellen. Der Typ der Variable ist dabei der hinzugefügte Stage, welcher von org.apache.deltaspike.core.api.projectstage.ProjectStage ableiten muss.
public class CustomProjectStage implements ProjectStageHolder {
  public static final class Debugging extends ProjectStage {
    private static final long serialVersionUID = -2626602281649294170L;
  }

  public static final Debugging Debugging = new Debugging();
}
Unseren neuen Debugging-Stage können wir bspw. in einem DeltaSpike Exception-Handler aus Listing Eigener Project-Stage in einem Exception-Handler verwenden, um IO-Exceptions nur ins Log zu schreiben, wenn der Debugging-Stage aktiviert ist. Der Exception-Handler Mechanismus von DeltaSpike erlaubt es explizit über das Event-API von CDI ein ExceptionToCatchEvent zu feuern. Listing Eigener Project-Stage in einem Exception-Handler zeigt einen entsprechenden Observer, der jedoch nicht auf dem Observer-API von CDI aufgebaut ist, da unter anderem durch ein eigenes Konzept von DeltaSpike die Abarbeitungsreihenfolge der Handler-Methoden optional festgelegt werden kann. Dieser und andere Aspekte erfordern eine etwas andere Umsetzung. Als erster Schritt muss die Handler-Klasse mit @ExceptionHandler markiert werden, damit ein Exception-Handler überhaupt als solcher registriert wird. Bei der Definition einer Handler-Methode selbst können wir den Regeln zu CDI-Observer-Methoden folgen. Statt @Observes müssen wir jedoch @Handles verwenden. Der Event-Typ ist mit ExceptionEvent ebenfalls anders, da bei einem CDI-Observer der ursprüngliche Event-Typ ( ExceptionToCatchEvent ) zu erwarten wäre. ExceptionEvent muss außerdem auf den zu überwachenden Exception-Typ typisiert werden und stellt zusätzliche Methoden zur Steuerung des Exception-Flows zur Verfügung. Da LoggingExceptionHandler Exceptions nur loggen soll, wird am Ende ExceptionEvent#throwOriginal aufgerufen. Sollte danach keine Handler-Methode #handled aufrufen, so wird die ursprüngliche Exception nach dem Aufruf aller zuständigen Exception-Handler weiter geworfen.
@ApplicationScoped
@ExceptionHandler
public class LoggingExceptionHandler {
  private static final Logger LOG =
    Logger.getLogger(LoggingExceptionHandler.class.getName());

  public void onUnhandledException(
      @Handles ExceptionEvent<IOException> exceptionEvent,
      ProjectStage projectStage) {

    if (projectStage == CustomProjectStage.Debugging) {
      LOG.log(Level.FINE,
        "exception detected", exceptionEvent.getException());
    }

    exceptionEvent.throwOriginal();
  }
}
DeltaSpike ruft Exception-Handler nicht automatisch beim Auftreten einer Exception auf. Stattdessen muss das zuvor erwähnte ExceptionToCatchEvent über das Event-API von CDI gefeuert werden. Listing Verwendung von ExceptionToCatchEvent zeigt die Verwendung in CustomJsonWriter .
@Provider
@Produces(MediaType.APPLICATION_JSON)
public class CustomJsonWriter implements MessageBodyWriter<Object> {
  //...

  @Inject
  private BeanManager beanManager;

  @Override
  public void writeTo(Object o, Class<?> rawType,
                      Type genericType,
                      Annotation[] annotations,
                      MediaType mediaType,
                      MultivaluedMap<String, Object> httpHeaders,
                      OutputStream entityStream) throws IOException {
    //...

    try {
      //...
    } catch (IOException e) {
      ExceptionToCatchEvent exceptionToCatchEvent =
        new ExceptionToCatchEvent(e);

      beanManager.fireEvent(exceptionToCatchEvent);
    }
  }

  //...
}
Da mit Methoden wie bspw. ExceptionEvent#abort die Verarbeitung abgebrochen werden kann ohne eine Exception zu werfen, stellt ExceptionToCatchEvent die Methode #isHandled zur Verfügung. Somit kann nach dem Feuern von ExceptionToCatchEvent überprüft werden, ob die Verarbeitung abgebrochen wurde oder ob die Exception von einem Handler tatsächlich behandelt wurde. Außerdem kann ExceptionToCatchEvent vor dem Feuern als optional markiert werden, um die Exception-Handler zu notifizieren, aber das automatische Werfen der Exception zu unterdrücken, falls diese nach dem Aufruf des letzten Handlers noch nicht behandelt wurde.
Tipp: DeltaSpike bietet weitere umfangreiche Möglichkeiten für den Umgang mit Exceptions. Diese sollten mit Bedacht eingesetzt werden, da die Behandlung von Exceptions sonst unübersichtlich werden kann.

5.3 Alles unter Kontrolle

Im vorherigen Abschnitt haben wir ein Request-scoped Bean zur Zwischenspeicherung und Aktualisierung von konfigurierten Werten verwendet. In der Praxis kann dies einen unnötig hohen Overhead verursachen. Konfigurierte Werte sind normalerweise eine bestimmte Zeit valide und müssen nicht ständig neu geladen werden. Allerdings kann es erforderlich werden zu bestimmten Zeitpunkten oder bei bestimmten Ereignissen solche Werte neu zu laden. Wir könnten einen der verfügbaren Scopes verwenden, der die gewünschten Eigenschaften hat um Konfigurationswerte zu speichern. Sollte es einen solchen Scope noch nicht geben, dann können wir einen eigenen definieren und implementieren. Auch hier hilft DeltaSpike mit der abstrakten Klasse org.apache.deltaspike.core.util.context.AbstractContext . Für IdeaFork können wir bspw. einen eigenen Config-Scope umsetzen, welcher manuell zurückgesetzt werden kann. Möchten wir eine entsprechende Annotation mit dem Namen @ConfigScoped verwenden, so müssen wir diese auf Basis der CDI-Regeln für Normal-Scopes definieren. Das Ergebnis ist in Listing Eigene Scope-Annotation ersichtlich. Da wir keinen passivierbaren Context benötigen, genügt es die Annotation @NormalScope ohne Anpassungen zu verwenden.
@NormalScope
@Target({TYPE, METHOD, FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface ConfigScoped {}
Eine Annotation alleine ist natürlich nicht ausreichend und daher implementieren wir im nächsten Schritt den dazugehörigen Kontext. Listing Eigene Kontext-Implementierung veranschaulicht, dass durch die Verwendung von AbstractContext eine eigene Implementierung sehr einfach umgesetzt werden kann. Die Methode #getScope gibt die Klasse unserer eben angelegten Annotation zurück, wodurch die Kontext-Implementierung mit dieser verknüpft wird. ContextualStorage ist eine vorgefertigte Datenstruktur zur Speicherung der Beans, welche im Konstruktor der Kontext-Implementierung auf einfache Art und Weise initialisiert werden kann. Wir müssen uns primär durch den zweiten Parameter entscheiden, ob die interne Datenstruktur parallele Zugriffe synchronisieren soll, um parallele Zugriffe korrekt zu unterstützen. In unserem Fall wollen wir dies und daher verwenden wir den Wert true . Durch die vordefinierte Getter-Methode namens #getContextualStorage kann die AbstractContext -Implementierung schließlich auf die aktuell gültige ContextualStorage -Instanz zugreifen. Die Methode #isActive gibt in unserem Fall immer true zurück, da der Kontext immer aktiv sein soll.

 

Weitere Methoden sind nicht durch javax.enterprise.context.spi.Context oder org.apache.deltaspike.core.util.context.AbstractContext vorgeschrieben und können daher selbst gewählt werden. Unser Config-Context soll eine Methode für einen vollständigen Reset zur Verfügung stellen, welche zu beliebigen Zeitpunkten manuell aufgerufen werden kann. In Listing Eigene Kontext-Implementierung delegiert hierfür die Methode #reset an die statische Helper-Methode AbstractContext#destroyAllActive , welcher das aktuell gültige ContextualStorage übergeben werden muss, um dessen Inhalt zurückzusetzen.
public class ConfigContext extends AbstractContext {
  private final ContextualStorage contextualStorage;

  public ConfigContext(BeanManager beanManager) {
    super(beanManager);
    contextualStorage =
      new ContextualStorage(beanManager, true, isPassivatingScope());
  }

  @Override
  protected ContextualStorage getContextualStorage(
      Contextual<?> contextual, boolean createIfNotExist) {

    return this.contextualStorage;
  }

  @Override
  public Class<? extends Annotation> getScope() {
    return ConfigScoped.class;
  }

  @Override
  public boolean isActive() {
    return true;
  }

  public void reset() {
    AbstractContext.destroyAllActive(this.contextualStorage);
  }
}
Wie jeder CDI-Context muss die Implementierung aus Listing Eigene Kontext-Implementierung mit Hilfe einer CDI-Extension registriert werden. Listing Eigene Kontext-Implementierung registrieren illustriert diesen Mechanismus. In einer Observer-Methode für das AfterBeanDiscovery -Event wird zusätzlich der BeanManager injiziert, welcher anschließend dem Konstruktor von ConfigContext übergeben wird. Die so erzeugte Context-Instanz wird abschließend über die Methode AfterBeanDiscovery#addContext registriert.
public class ConfigContextExtension implements Extension {
  public void registerDeltaSpikeContexts(
      @Observes AfterBeanDiscovery afterBeanDiscovery,
      BeanManager beanManager) {

    ConfigContext configContext = new ConfigContext(beanManager);
    afterBeanDiscovery.addContext(configContext);
  }

  public void shutdownConfigContext(
      @Observes BeforeShutdown beforeShutdown,
      BeanManager beanManager) {

    ((ConfigContext)beanManager.getContext(ConfigScoped.class)).reset();
  }
}
Wie bei CDI üblich, müssen CDI-Extensions in der Datei META-INF/services/javax.enterprise.inject.spi.Extension vollständig qualifiziert eingetragen werden. Bei der vorliegenden CDI-Extension ist der neue Inhalt der Konfigurationsdatei: at.irian.cdiatwork.ideafork.core.impl.config.context.ConfigContextExtension

 

In Listing Verwendung einer eigenen Scope-Annotation kann die Scope-Annotation des hiermit registrierten CDI-Kontextes folglich für unseren Konfigurationszwischenspeicher namens MonitoringConfig eingesetzt werden.
@ConfigScoped
public class MonitoringConfig {
  @Inject
  @ConfigProperty(name = "methodInvocationThreshold")
  private Integer methodInvocationThreshold;

  public Integer getMethodInvocationThreshold() {
    return methodInvocationThreshold;
  }
}

 

In Listing Eigene Kontext-Implementierung registrieren wird die #reset -Methode von ConfigContext in der Observer-Methode für das BeforeShutdown -Event aufgerufen, um evt. vorhandene @PreDestroy -Callbacks der gespeicherten Beans aufzurufen. Wäre dies der einzige Aufruf der Reset-Methode, so würde sich unser neu implementierte Kontext wie der standard Application-Context von CDI verhalten. In IdeaFork soll der Config-Context über verschiedene Mechanismen zurückgesetzt werden können. In diesem Abschnitt möchten wir hierfür JMX verwenden. DeltaSpike-Core erlaubt nämlich durch die Verwendung der Annotation @MBean CDI-Beans automatisch als JMX-Beans zu registrieren. Die Angabe eines Bean-Namens und einer Kategorie für JMX ist optional. Listing CDI-Bean als JMX-Bean aktivieren veranschaulicht, wie wir beide Informationen explizit festlegen können. Dadurch wird das CDI-Bean bspw. in der JMX-Konsole namens jconsole sichtbar. Die Klasse ConfigReloader definiert nur eine Methode, die zusätzlich mit @JmxManaged annotiert ist. Diese Annotation markiert Methoden, welche über JMX aufrufbar sein sollen. In der Methodenimplementierung holen wir uns über den injizierten BeanManager unseren selbst implementierten Kontext, um die Methode #reset aufzurufen.
@ApplicationScoped
@MBean(name = "ConfigReloader", category = "IdeaFork")
public class ConfigReloader {
  @Inject
  private BeanManager beanManager;

  @JmxManaged
  public void reloadConfig() {
    ((ConfigContext)beanManager.getContext(ConfigScoped.class))
      .reset();
  }
}
Als Ergebnis ist es jetzt möglich zu beliebigen Zeitpunkten den Zwischenspeicher für konfigurierte Werte via JMX zurückzusetzen, wodurch sämtliche Werte, die in einem @ConfigScoped CDI-Bean abgelegt sind, beim nächsten Zugriff neu geladen werden.

5.4 Helfende Hände

DeltaSpike-Core stellt neben Basisklassen auch einige statische Helper-Methoden zur Verfügung. In IdeaFork haben wir in CdiUtils bspw. zwei statische Methoden umgesetzt, die wir ersetzen können. Die erste dieser beiden Methoden heißt #injectFields und wird in CustomJsonWriter , IdeaExporter und RestApplicationConfig verwendet, um Injection-Points der aktuellen Instanz manuell zu befüllen. Eine äquivalente Methode wird durch den sogenannten BeanProvider von DeltaSpike zur Verfügung gestellt. Listing Manuelle Injizierung zeigt die Verwendung in der Klasse IdeaExporter . Die Umstellung selbst ist sehr einfach, da nur der Klassennamen von CdiUtils auf BeanProvider geändert werden muss.
private synchronized void init() {
  if (ideaManager == null) {
    BeanProvider.injectFields(this);
  }
}
Die Klasse BeanProvider stellt darüber hinaus viele weitere Helper-Methoden bereit. Die zweite Methode namens #getContextualReference , die wir bisher in CdiUtils manuell implementiert haben, kann ebenfalls durch eine gleichnamige Version von BeanProvider ersetzt werden. Listing Manueller Lookup zeigt die Umstellung in BeanAwareConstraintValidatorFactory auf einen optionalen Lookup via BeanProvider .
@Override
public <T extends ConstraintValidator<?, ?>> T
    getInstance(Class<T> validatorClass) {

  T managedConstraintValidator =
    BeanProvider.getContextualReference(validatorClass, true);

  if (managedConstraintValidator == null) {
    managedConstraintValidator =
      this.defaultFactory.getInstance(validatorClass);
  }
  return managedConstraintValidator;
}
Die Methode #getContextualReference ist in BeanProvider mehrfach überladen, wodurch verschiedene Parameterkombinationen verwendet werden können. Neben optionalen Qualifiern kann ein solcher Lookup auch mit dem Namen eines Beans erfolgen, sofern dieser definiert wurde. Dennoch sollte ein typsicherer Lookup bevorzugt werden. Manuelle Lookups sollten jedoch mit großer Sorgfalt eingesetzt werden. Normal-scoped Beans sind hierbei unproblematisch, weil nur die Contextual-Reference und nicht die Contextual-Instance vom CDI-Container nach außen gegeben wird. Für dependent-scoped Beans gilt dies aber nicht und daher stehen separate Lookup-Methoden unter dem Namen #getDependent zur Verfügung. Das Ergebnis wird in eine Datenstruktur namens DependentProvider verpackt, damit eine korrekte manuelle Zerstörung der dependent-scoped Instanz zu einem späteren Zeitpunkt möglich ist. Würde ein dependent-scoped Bean in ein normal-scoped Bean injiziert, dann würde der CDI-Container das dependent-scoped Bean zerstören, sobald das dazugehörige normal-scoped Bean zerstört wird. Dieser Aufgabe kann der CDI-Container bei einem direkten Lookup eines dependent-scoped Beans nicht automatisch nachkommen und daher ist es erforderlich diesen Prozess explizit anzustoßen.

 

In der Klasse ActiveUserHolder werfen wir bei einem Session-Timeout im @PreDestroy -Callback ein UserActionEvent . Da hier kein (HTTP-)Request aktiv ist, können bspw. Request-scoped Beans nicht verwendet werden. Ursprünglich war MonitoringConfig ein Request-scoped Bean und wäre für einen solchen Anwendungsfall nicht einsetzbar, da es zu einer ContextNotActiveException gekommen wäre. Durch die Verwendung von @ConfigScoped müssten wir in IdeaFork aktuell diesen Fall nicht berücksichtigen.

 

Da Session-Timeouts und deren Folgen oftmals bei Applikationstests vernachlässigt werden, können wir dennoch in ActiveUserHolder Vorkehrungen treffen, dass es hier zu einem späteren Zeitpunkt zu keinen Problemen kommen kann. Listing Manuelle Verwendung von dependent-scoped Beans enthält gleich mehrere Aspekte, die in solchen und ähnlichen Fällen interessant sind.

 

Statt den BeanManager zu injizieren, kann dieser in einem ersten Schritt auch über den BeanManagerProvider von DeltaSpike geholt werden. Dies eignet sich vor allem für die Verwendung in statischen Methoden, sowie für die Verwendung in Instanzen, die nicht durch den CDI-Container verwaltet werden. Mit Hilfe des BeanManager s und von BeanProvider#getDependent wird im nächsten Schritt ein dependent-scoped Bean vom Typ ContextControl abgerufen. Dieses Interface ist nicht in DeltaSpike-Core enthalten, sondern in einem separaten Teil von DeltaSpike namens CDI-Control. Rein technisch wäre der Umweg über DependentProvider nicht erforderlich, da die verfügbaren Implementierungen keine @PreDestroy -Callbacks verwenden. Dennoch ist die in Listing Manuelle Verwendung von dependent-scoped Beans gezeigte Verwendung sinnvoll, da DeltaSpike sonst Warnungen ins Log schreibt.

 

Nach dem Lookup via BeanProvider#getDependent kann auf die Contextual-Instance selbst über DependentProvider#get zugegriffen werden. In unserem Fall starten wir den Request-Context bevor UserActionEvent gefeuert wird und beenden ihn bevor wir die dependent-scoped Instanz von ContextControl mit Hilfe von DependentProvider#destroy wieder zerstören.
Tipp: Im Hintergrund wird ein gemockter Request mit dem aktuellen Thread verbunden, wodurch bis zum Stopp des Request-Contexts beliebige Request-scoped CDI-Beans wiederverwendet werden können. Durch Konzepte wie diese können alle Standardkontexte auch in einer CDI-basierten Java SE Applikation bzw. in Unit-Tests verwendet werden. Indirekt verwenden wir diesen Vorteil seit dem ersten Beispiel, da das Test-Control-Modul intern ebenfalls CDI-Control verwendet, um den CDI-Container zu starten und zu stoppen bzw. die Standardkontexte je nach Anforderung zu kontrollieren.
public void onLogout(User user, boolean manualLogout) {
  if (manualLogout) {
    userActionEvent
      .fire(new UserActionEvent(new UserAction(LOGOUT, user)));
  } else {
    BeanManager beanManager =
      BeanManagerProvider.getInstance().getBeanManager();

    DependentProvider<ContextControl> contextControlProvider =
      BeanProvider.getDependent(beanManager, ContextControl.class);

    try {
      contextControlProvider.get().startContext(RequestScoped.class);

      userActionEvent
        .fire(new UserActionEvent(new UserAction(AUTO_LOGOUT, user)));
    } finally {
      contextControlProvider.get().stopContext(RequestScoped.class);
      contextControlProvider.destroy();
    }
  }
}
Tipp: Seit Version 1.1 stellt CDI mit CDI.current().getBeanManager() einen Ersatz für den BeanManagerProvider zur Verfügung.
Neben diesen sehr CDI spezifischen Hilfsmitteln enthält DeltaSpike-Core auch allgemeinere Werkzeuge wie bspw. ProxyUtils und AnnotationUtils . In IdeaFork haben wir die Erkennung von Proxy-Klassen bisher manuell gemacht. Listing Manuelle Auswertungen zeigt das bisherige Vorgehen in DefaultMonitoredInterceptorStrategy , welches in Listing Utility Methoden von DeltaSpike durch die Verwendung von ProxyUtils#getUnproxiedClass ersetzt werden kann.
private Monitored extractMonitoredAnnotation(InvocationContext ic) {
  Monitored result = ic.getMethod().getAnnotation(Monitored.class);

  if (result != null) {
    return result;
  }

  Class<?> targetClass = ic.getTarget().getClass();

  if (targetClass.getName()
        .startsWith(targetClass.getSuperclass().getName()) &&
      targetClass.getName().contains("$$")) {

    targetClass = targetClass.getSuperclass();
  }

  result = targetClass.getAnnotation(Monitored.class);

  if (result == null) {
    return findAnnotation(
      beanManager, targetClass.getAnnotations(), Monitored.class);
  }

  return result;
}

private static <T extends Annotation> T findAnnotation(
    BeanManager beanManager,
    Annotation[] annotations,
    Class<T> targetAnnotationType) {

  for (Annotation annotation : annotations) {
    if (targetAnnotationType.equals(annotation.annotationType())) {
      return (T) annotation;
    }
    if (beanManager.isStereotype(annotation.annotationType())) {
      T result = findAnnotation(
        beanManager,
        annotation.annotationType().getAnnotations(),
        targetAnnotationType);
      if (result != null) {
        return result;
      }
    }
  }
  return null;
}
Eine weitere Hilfsklasse, die in Listing Utility Methoden von DeltaSpike verwendet wird, ist AnnotationUtils . In DefaultMonitoredInterceptorStrategy lässt sich die manuell implementierte Methode #findAnnotation mit AnnotationUtils#findAnnotation ersetzen. Ein zusätzlicher Vorteil von AnnotationUtils#findAnnotation ist die Unterstützung von CDI-Stereotypen. Dies ist auch der Grund, warum der BeanManager als erster Parameter übergeben werden muss.
private Monitored extractMonitoredAnnotation(InvocationContext ic) {
  Monitored result = ic.getMethod().getAnnotation(Monitored.class);

  if (result != null) {
    return result;
  }

  Class<?> targetClass = ic.getTarget().getClass();

  targetClass = ProxyUtils.getUnproxiedClass(targetClass);

  result = targetClass.getAnnotation(Monitored.class);

  if (result == null) {
    return AnnotationUtils.findAnnotation(
      beanManager, targetClass.getAnnotations(), Monitored.class);
  }

  return result;
}
DeltaSpike-Core enthält viele interessante Hilfsmittel wie diese. Ein Blick in das Package org.apache.deltaspike.core.util ist sehr empfehlenswert. Selbst für Hilfsmittel, die nicht direkt im Zusammenhang mit CDI selbst stehen.

5.5 Sicher ist sicher

Neben Hilfsmitteln rund um CDI stellt DeltaSpike auch neue Konzepte für andere Spezifikationen wie bspw. JSF zur Verfügung, um die Entwicklung von Applikationen durch zusätzliche Typsicherheit zu erleichtern und die Wartbarkeit zu verbessern. Ein Beispiel hierfür ist die View-Config. Dieser Mechanismus erlaubt die typsichere Konfigurationen von (JSF-)Seiten.
Tipp: View-Configs sind derzeit speziell für JSF implementiert. Das Konzept selbst ist jedoch unabhängig von JSF und daher enthält DeltaSpike-Core die meisten Interfaces und Annotationen. Somit sind auch Implementierungen für andere UI-Frameworks auf Basis von DeltaSpike-Core möglich. Das JSF-Modul von DeltaSpike stellt eine Implementierung für JSF zur Verfügung und ermöglicht zusätzlich die Verwendung der optionalen Annotationen @View und @Folder .
Wie eingangs beschrieben fügen wir jetzt in IdeaFork das JSF-Modul hinzu, damit wir für beliebige JSF-Seiten eine typsichere View-Config anlegen können. Listing Minimale View-Config veranschaulicht die einfachste Variante ohne zusätzliche Metadaten.
public class Index implements ViewConfig {
}
Ohne eine zusätzliche Verwendung in der Applikation führt die Konfiguration aus Listing Minimale View-Config nur zu einer Pfad-Validierung. Die zuvor gezeigte Seitenkonfiguration definiert den Dateipfad /index.xhtml . In IdeaFork ist diese Datei allerdings nicht vorhanden. Wird eine Anwendung mit einer ungültigen Seitenkonfiguration gestartet, dann bricht DeltaSpike den Startvorgang ab und meldet eine ungültige Konfiguration. Die Datei index.xhml ist in unserem Fall im Verzeichnis pages abgelegt. Entsprechend ist die in Listing Minimale View-Config in einem Verzeichnis dargestellte View-Config erforderlich.
public interface Pages {
  class Index implements ViewConfig {}
}
Verzeichnisse werden durch verschachtelbare Interfaces repräsentiert und konkrete Seiten durch Klassen, die direkt oder indirekt das Interface ViewConfig implementieren. Bei der Konvertierung in eine JSF View-ID wird jeweils der erste Buchstabe in einen Kleinbuchstaben umgewandelt und für JSF-Seiten wird ein Suffix hinzugefügt, wodurch im Falle von Listing Minimale View-Config in einem Verzeichnis der Pfad /pages/index.xhtml entsteht.

 

Abgesehen von der automatischen Validierung der Pfade kann diese typsichere Konfiguration auch ohne zusätzliche Metadaten bereits sinnvoll verwendet werden. View-Configs können nämlich zusätzlich für eine typsichere JSF-Navigation verwendet werden. Listing Minimale typsichere JSF-Navigation veranschaulicht dies anhand einer Action-Methode die im Gegensatz zu einer herkömmlichen Action-Methode keinen String als Return-Typ verwendet.
public Class<? extends ViewConfig> onJsfAction() {
  //...
  return Pages.Index.class;
}
DeltaSpike konvertiert Pages.Index.class automatisch in /pages/index.xhtml , wodurch die JSF Implementierung eine normale View-ID als Navigationsziel erhält und sich somit wie bei einer standardmäßigen impliziten JSF-Navigation verhält, welche seit JSF 2.0 von der Spezifikation unterstützt wird.

 

Wirklich sinnvoll wird das View-Config Konzept in Kombination mit zusätzlichen Metadaten. Listing Verzeichnishierarchie mit View-Configs zeigt eine Konfiguration wie sie oft in der Praxis verwendet wird. Mit @View lassen sich JSF-spezifische Informationen wie der Navigationsmodus festlegen und explizite Namen vergeben. @View muss jedoch nicht für jede Seite erneut definiert werden, sondern kann über die Vererbungshierarchie vererbt werden. Da eine Seite nur indirekt ViewConfig implementieren muss, wird in Listing Verzeichnishierarchie mit View-Configs Pages von ViewConfig abgeleitet, wodurch alle anderen Konfigurationen keine direkte Verbindung zu einem der Interfaces von DeltaSpike benötigen. Durch die Vererbungshierarchie wird @View an alle Seitenkonfigurationen vererbt, die direkt oder indirekt das Pages -Interface implementieren.

 

Die Angabe von REDIRECT als Navigationsmodus verändert die generierten Navigationsstrings. So wird bspw. aus /pages/index.xhtml der Wert /pages/index.xhtml?faces-redirect=true . Gleiches gilt auch für alle andere Seiten mit Ausnahme von Pages.User.Login.class , da nur diese Seitenkonfiguration nicht das Interface Pages implementiert.

 

Pages.User.Login.class erweitert stattdessen die Klasse org.apache.deltaspike.core.api.config.view.DefaultErrorView . Diese Markerklasse für die Error-Seite einer Applikation darf nur von einer Konfigurationsklasse erweitert werden. DeltaSpike benötigt diesen Marker, um generisch zur Default-Error-Seite einer Applikation zu navigieren, falls es in der Applikation zu einem unbehandelten Fehler kommt. Soll zu einem späteren Zeitpunkt eine andere Seite als Error-Seite verwendet werden, dann müssen nur die betroffenen Konfigurationsklassen entsprechend angepasst werden.

 

In Listing Verzeichnishierarchie mit View-Configs ist ebenfalls ersichtlich, dass User als Interface im Pages -Interface verschachtelt ist. Dies ist immer dann erforderlich, wenn es einen Unterordner im Dateisystem gibt. Somit spiegelt sich die Struktur des Dateisystems in der Konfiguration wider. Würden wir etwas später nur einen der Ordern umbenennen und die typsichere Konfiguration nicht entsprechend nachziehen, dann würde der nächste Applikationsstart mit einer Exception enden. Da es sich bei der View-Config für die Pfadkonfiguration um Interfaces und Klassen handelt ist eine Aktualisierung sehr einfach möglich. Alle modernen Java-IDEs können Klassen- bzw. Interface-Namen automatisch im gesamten Projekt aktualisieren. Spätestens der Java Compiler überprüft ob alle Verweise korrekt geändert wurden.
@View(navigation = REDIRECT)
public interface Pages extends ViewConfig {
  class Index implements Pages {}

  interface User extends Pages {
    class Login extends DefaultErrorView {}

    class Registration implements User {}

    class Profile implements User {}
  }
}
Tipp: Die zuvor beschriebene Namenskonvention kann angepasst werden. Eine einfache Anpassung werden wir uns im nächsten Abschnitt dieses Kapitels ansehen.
Listing Seitenkonfigurationen für IdeaFork zeigt die Seitenkonfiguration von IdeaFork für die bestehenden JSF-Seiten. Wir müssten nicht für jede Seite zwingend eine View-Config anlegen, aber für jede Konfigurationsklasse muss die dazugehörige JSF-Seite existieren. Daher ist es grundsätzlich möglich auf die Konfiguration von konkreten Seiten zu verzichten. Werden nur Verzeichnisse konfiguriert, so ist es möglich bspw. Security-Constraints für gesamte Ordner zu definieren. In IdeaFork werden wir als einen der nächsten Schritte eine Kombination verwenden, indem wir Security-Constraints auf Ordner-Ebene definieren und für bestimmte Seiten zusätzliche Metadaten hinterlegen.
@View(navigation = REDIRECT)
public interface Pages extends ViewConfig {
  class Index implements Pages {}

  interface User extends Pages {
    class Login extends DefaultErrorView {}

    class Registration implements User {}

    class Profile implements User {}
  }

  interface Idea extends Pages {
    class Overview implements Idea {}

    class Create implements Idea {}
    class Edit implements Idea {}

    class List implements Idea {}
    class Details implements Idea {}
  }

  interface Search extends Pages {
    class Fork implements Search {}
  }

  interface Import extends Pages {
    class Upload implements Import {}
    class Summary implements Import {}
  }
}
Da Verzeichnisse durch Interfaces repräsentiert sind und somit einen eigenen Typ haben, kann das Navigationsziel sogar durch den Return-Typ von Action-Methoden eingeschränkt werden. Listing Navigationsziel einschränken via Return-Typ zeigt eine der umgestellten Action-Methoden von IdeaFork . Statt Class<? extends ViewConfig> wird Class<? extends Pages.Idea> verwendet. Da hier unsere eigenen Interfaces verwendet werden, wird die Implementierung lesbarer und zusätzlich stellt der Java Compiler sicher, dass das Navigationsziel im Verzeichnis /pages/idea liegen muss.
@ViewController
public class IdeaCreateViewCtrl implements Serializable {
  //...

  public Class<? extends Pages.Idea> save() {
    //...
    ideaService.save(ideaToSave);
    return Pages.Idea.Overview.class;
  }
}
Rein technisch sind auch Mischungen möglich. Listing Möglichkeiten für Return-Typen stellt die entsprechend angepasste Version der Klasse MenuController dar. Hier wird ersichtlich, dass wie bei der Methode #home durch die Angabe von Class<? extends Pages> das oberste Basisverzeichnis als Navigationsziel festgelegt werden kann. Die Navigation selbst kann zu einer Seite in diesem Verzeichnis oder zu einer Seite in einem der Unterverzeichnisse durchgeführt werden. Als Alternative kann auch der komplette Pfad bereits durch den Return-Typ vorgegeben werden. Dies wird bspw. bei der Methode #login umgesetzt. Im Gegensatz hierzu stehen die Methoden #logout und #start , bei welchen zu jeder gültigen View-Config Konfiguration navigiert werden kann.
@Named("menuBean")
@Model
public class MenuController {
  @Inject
  private ActiveUserHolder userHolder;

  public Class<? extends Pages> home() {
    return Pages.Index.class;
  }

  public Class<Pages.User.Login> login() {
    return Pages.User.Login.class;
  }

  public Class<? extends ViewConfig> logout() {
    userHolder.setAuthenticatedUser(null);
    return Pages.User.Login.class;
  }

  public Class<? extends ViewConfig> start() {
    if (userHolder.isLoggedIn()) {
      return Pages.Idea.Overview.class;
    }
    return Pages.User.Login.class;
  }

  public Class<? extends Pages.User> register() {
    return Pages.User.Registration.class;
  }
}
Abgesehen von JSF Action-Methoden können typsichere Ordner- und Seitenkonfigurationen auch außerhalb von JSF, bspw. für eine typsichere Navigation, verwendet werden. Listing Verwendung von ViewConfigResolver illustriert wie IdeaImportServlet in IdeaFork von diesem Konzept profitieren kann. Im Vergleich zu der bisherigen Implementierung dieses Servlets kann ein sogenannter ViewConfigResolver injiziert werden. Über die Methode #getViewConfigDescriptor können wir mit einer Pfadangabe als String oder einer Klasse vom Typ ViewConfig die dazugehörige Konfiguration inklusive aller Metadaten abfragen. In unserem Fall holen wir uns die Konfiguration für Pages.Import.Summary.class und rufen die Methode #getViewId auf den resultierenden Descriptor auf, um die Pfadangabe als String zu erhalten.
@WebServlet("/idea/import")
@MultipartConfig
public class IdeaImportServlet extends HttpServlet {
  @Inject
  private ActiveUserHolder userHolder;

  @Inject
  private FileUploadService fileUploadService;

  @Inject
  private ViewConfigResolver viewConfigResolver;

  protected void doPost(HttpServletRequest request,
                        HttpServletResponse response)
        throws ServletException, IOException {

    fileUploadService.storeUploadedFiles(
      request.getParts(), userHolder.getAuthenticatedUser());

    ViewConfigDescriptor viewConfigDescriptor =
      viewConfigResolver.getViewConfigDescriptor(
        Pages.Import.Summary.class);

    request.getRequestDispatcher(viewConfigDescriptor.getViewId())
      .forward(request, response);
  }
}
Bei zukünftigen Refactorings müssen wir somit keinen fix definierten String mehr manuell nachziehen. Darüber hinaus vereinfachen moderne Java IDEs nicht nur das Refactoring selbst, sondern auch die Suche nach Verweisen auf bestimmte Seiten. Im Git-Repository von IdeaFork sind sämtliche Änderungen zu diesem Thema in einem Commit zusammengefasst und die gesamte Applikation ist somit auf View-Configs umgestellt.
Tipp: Vererbte Metadaten können überschieben oder erweitert werden. So ist es möglich bspw. @View bei einer konkreten Seitenkonfiguration zu verwenden, um für einzelne Seiten das Verhalten anzupassen oder die vererbten Informationen mit zusätzlichen Angaben zu erweitern.
Die zuvor definierte Error-Seite haben wir in IdeaFork noch nicht explizit verwendet. Ein naheliegendes Einsatzgebiet ist die Fehlerbehandlung von bestimmten Exceptions. Das Exception-Handling Konzept von DeltaSpike haben wir in einem kurzen Beispiel bereits kennengelernt. Im nächsten Schritt wollen wir diese und weitere Mechanismen kombinieren, um bei unbehandelten Exceptions vom Typ IllegalStateException die festgelegte Error-Seite anzuzeigen.

 

In Listing Exception-Handler mit Navigation zu DefaultErrorView werden unbehandelte Exceptions vom Typ IllegalStateException als behandelt markiert. Zusätzlich wird in dem Request-scoped Exception-Handler das Flag exceptionDetected in einem solchen Fall auf true gesetzt. Der Null-Check für den FacesContext ist erforderlich, da Request-scoped CDI Beans auch außerhalb eines JSF-Requests aktiviert werden können. Schließlich stellt @Handles(ordinal = Integer.MIN_VALUE) sicher, dass die Handler-Methode am Ende der Handler-Kette aufgerufen wird.

 

In einem weiteren Schritt erhält die Klasse ErrorViewAwareExceptionHandler einen CDI-Observer mit dem Qualifier @BeforePhase(JsfPhaseId.RENDER_RESPONSE) für den Event-Typ PhaseEvent . Wurde im aktuellen Request das Flag exceptionDetected auf true gesetzt, so kann mit ViewNavigationHandler#navigateTo in Kombination mit einer Konfigurationsklasse zu einer JSF-Seite navigiert werden. Wir wollen jedoch nicht zu einer fix definierten Seite navigieren, sondern zu der aktuell konfigurierten Error-Seite. Aus diesem Grund wird DefaultErrorView.class als Argument übergeben. Da DeltaSpike diesen Marker kennt wird im Hintergrund die Seitenkonfiguration gesucht, welche von dieser Marker-Klasse ableitet. Sofern es eine solche Seitenkonfiguration gibt, wird intern der Pfad, der durch die Konfiguration repräsentiert wird, für die effektive Navigation verwendet.
Tipp: DeltaSpike definiert mit @BeforePhase und @AfterPhase zwei Qualifier, welche in Kombination mit jeder beliebigen Phase des JSF Request-Lifecycles verwendet werden können.
@RequestScoped
@ExceptionHandler
public class ErrorViewAwareExceptionHandler {
  private boolean exceptionDetected = false;

  public void onUnhandledException(
      @Handles(ordinal = Integer.MIN_VALUE)
      ExceptionEvent<IllegalStateException> exceptionEvent) {

    FacesContext facesContext = FacesContext.getCurrentInstance();

    if (facesContext == null) {
      return;
    }

    if (!exceptionEvent.isMarkedHandled()) {
      exceptionEvent.handled();
      exceptionDetected = true;
    }
  }

  protected void navigateOnDetectedException(
      @Observes @BeforePhase(JsfPhaseId.RENDER_RESPONSE)
      PhaseEvent phaseEvent,
      ViewNavigationHandler viewNavigationHandler) {

    if (exceptionDetected) {
      viewNavigationHandler.navigateTo(DefaultErrorView.class);
    }
  }
}
Neben den bisher vorgestellten Metadaten für View-Configs integriert das JSF-Modul auch die @Secured -Annotation des Security-Moduls von DeltaSpike. Grundsätzlich handelt es sich hier um eine Art Interceptor, mit welchem Klassen oder einzelne Methoden annotiert werden können. Bei diesem Interceptor muss mindestens eine Implementierung von AccessDecisionVoter angegeben werden, welche verwendet wird um den Zugriff auf die auszuführende Methode zu überprüfen. In Verbindung mit dem View-Config Konzept wird kein Methodenaufruf abgesichert, sondern das jeweils konfigurierte Verzeichnis oder einzelne Seiten.

 

In Listing Absicherung von Seiten mit @Secured wird der View-Config von IdeaFork ein zusätzliches Marker-Interface mit dem Namen SecuredPages hinzugefügt. SecuredPages sieht wie die Konfiguration eines Verzeichnisses aus. Tatsächlich handelt es sich jedoch um ein Interface zur Sammlung von Metadaten. Technisch gesehen könnte dieses Interface auch separat definiert werden. In unserem Fall erweitert SecuredPages das Interface Pages, um dessen Metadaten zu übernehmen. Außerdem wird SecuredPages mit der Annotation @Secured versehen. Alle Verzeichniskonfigurationen, die von SecuredPages ableiten, werden durch den UserAwareAccessDecisionVoter abgesichert, da dieser bei @Secured angegeben ist und entsprechend vererbt wird. Sofern Klassen für die Seitenkonfiguration in solchen Verzeichnissen vorhanden sind und eine mit @Secured gesicherte Verzeichniskonfiguration implementieren, erben auch diese Seitenkonfigurationen lt. der allgemeinen View-Config-Regel die Definition von @Secured . In diesen Fällen wird nicht nur der Zugriff auf das Verzeichnis geprüft, sondern auch auf einzelne Seiten, die mit View-Config Klassen abgebildet werden. Listing Absicherung von Seiten mit @Secured veranschaulicht durch Pages.User.Profil.class , dass auch einzelne Seiten abgesichert werden können, selbst wenn sie nicht in einem abgesicherten Verzeichnis enthalten sind.
@View(navigation = REDIRECT)
public interface Pages extends ViewConfig {
  class Index implements Pages {}

  @Secured(UserAwareAccessDecisionVoter.class)
  interface SecuredPages extends Pages {}

  interface User extends Pages {
    class Login extends DefaultErrorView {}

    class Registration implements User {}

    class Profile implements SecuredPages {}
  }

  interface Idea extends SecuredPages {
    class Overview implements Idea {}

    class Create implements Idea {}
    class Edit implements Idea {}

    class List implements Idea {}
    class Details implements Idea {}
  }

  interface Search extends SecuredPages {
    class Fork implements Search {}
  }

  interface Import extends SecuredPages {
    class Upload implements Import {}
    class Summary implements Import {}
  }
}
Auch SecuredPages kann durch die indirekte Erweiterung von ViewConfig zur Einschränkung der Navigationsziele verwendet werden. Listing SecuredPages als Navigationsziel zeigt den auf View-Configs umgestellten NavigationController von IdeaFork . Die Methode #toUserProfile definiert Class<? extends Pages.SecuredPages> als Return-Typ, wodurch nur zu Seiten in gesicherten Verzeichnissen navigiert werden kann.
@Named
@ApplicationScoped
public class NavigationController {
  public Class<? extends Pages.Idea> toNewIdea() {
    return Pages.Idea.Create.class;
  }

  public Class<? extends Pages.Idea> toIdeaList() {
    return Pages.Idea.List.class;
  }

  public Class<? extends Pages.Import> toIdeaImport() {
    return Pages.Import.Upload.class;
  }

  public Class<? extends Pages.SecuredPages> toUserProfile() {
    return Pages.User.Profile.class;
  }
}
Ein AccessDecisionVoter kann entweder wie UserAwareAccessDecisionVoter an eine eigene Security-Logik delegieren oder die Überprüfung an ein beliebiges Security-Framework weiterleiten. In Listing AccessDecisionVoter mit typsicheren Nachrichten delegieren wir an das ActiveUserHolder -Bean von IdeaFork .

 

Wird @Secured über eine Verzeichniskonfiguration an Seitenkonfigurationen vererbt, dann wird jeder AccessDecisionVoter , der durch @Secured referenziert wird, mehrfach aufgerufen. Der erste Aufruf wird für die Seite selbst durchgeführt und anschließend erfolgt je Verzeichnisebene, die @Secured vererbt bekommen hat, ein eigener Aufruf. Je Aufruf wird ein manueller Bean-Lookup mit der angegebenen AccessDecisionVoter -Klasse durchgeführt. Beispielsweise wird im Falle von Pages.Idea.Overview.class die Methode UserAwareAccessDecisionVoter#checkPermission für Pages.Idea.Overview.class und Pages.Idea.class auf das gefundene CDI-Bean aufgerufen. Die Aufrufe für die Zugriffskontrolle von Verzeichnissen und Seiten unterscheiden sind nur durch den Inhalt der Metadaten, auf welche mit der Methode AccessDecisionVoterContext#getMetaData zugegriffen werden kann.
@RequestScoped
public class UserAwareAccessDecisionVoter
    extends AbstractAccessDecisionVoter {

  @Inject
  private ActiveUserHolder activeUserHolder;

  @Inject
  private UserMessage userMessage;

  @Override
  protected void checkPermission(
      AccessDecisionVoterContext accessDecisionVoterContext,
      Set<SecurityViolation> securityViolations) {

    if (!activeUserHolder.isLoggedIn()) {
      securityViolations.add(
        newSecurityViolation(userMessage.pleaseLogin()));
    }
  }
}
Listing AccessDecisionVoter mit typsicheren Nachrichten zeigt neben einem einfachen AccessDecisionVoter zusätzlich die Verwendung von typsicheren Messages. UserMessage ist ein eigenes Interface, welches mit @MessageBundle annotiert ist. Listing Definition typsicherer Nachrichten zeigt einen Ausschnitt von UserMessage . Jede Methode definiert einen Key der in einem Resource-Bundle vorhanden sein muss. Wird der Name des Resource-Bundles nicht explizit angegeben, dann entspricht der Name des Bundles dem vollständig qualifizierten Namen des Interfaces. Soll der Namen des Keys anders sein, so kann auch dieser explizit angegeben werden. Lautet der Key bspw. please_login statt pleaseLogin , dann kann die Methode mit @MessageTemplate("{please_login}") annotiert werden. Alternativ können Texte wie in Listing Definition typsicherer Nachrichten fix angegeben werden.
@MessageBundle
public interface UserMessage {
    @MessageTemplate("Welcome %s!")
    String welcomeNewUser(String nickName);

    @MessageTemplate("Login failed!")
    String loginFailed();

    @MessageTemplate("Please login")
    String pleaseLogin();

    //...
}
Die Methode #welcomeNewUser illustriert zusätzlich, dass Message-Parameter mit Hilfe von Methodenparametern befüllt werden können. Im Nachrichtentext sind beliebig viele Platzhalter ("%s") erlaubt, welche der Reihenfolge nach mit den Werten ersetzt werden die der Methode als Argumente übergeben werden. Handelt es sich bei einem Parameter-Typ nicht um einen String, dann wird die Methode #toString aufgerufen. Somit ist eine typsichere Parametrisierung möglich.

 

Für JSF gibt es darüber hinaus eine Erweiterung dieses Konzepts, welche in Listing Typsichere JSF-Messages veranschaulicht wird. Im Gegensatz zu der direkten Injizierung von UserMessage und Verwendung von #pleaseLogin aus dem vorherigen Beispiel wird in diesem Fall ein vom JSF-Modul zur Verfügung gestelltes Interface namens JsfMessage injiziert, welches auf UserMessage typisiert wird. Dies ermöglicht, über Methoden wie bspw. #addInfo und #addError , die implizite Erzeugung entsprechender Faces-Messages. In unserem Beispiel wird der Text für #welcomeNewUser auf der Oberfläche als Informationsmeldung angezeigt. Im Hintergrund verwendet DeltaSpike das Locale, welches für den aktuellen JSF-Request aktiv ist, und fügt die erzeugte FacesMessage -Instanz dem aktuellen FacesContext hinzu.
@ViewController
public class LoginViewCtrl {
  //...

  @Inject
  private JsfMessage<UserMessage> userMessage;

  public Class<? extends Pages.Idea> login() {
    userService.login(email, password);

    final Class<? extends Pages.Idea> navigationTarget;
    if (userHolder.isLoggedIn()) {
      userMessage.addInfo()
        .welcomeNewUser(
          userHolder.getAuthenticatedUser().getNickName());
      navigationTarget = Pages.Idea.Overview.class;
    } else {
      userMessage.addError().loginFailed();
      navigationTarget = null;
    }

    return navigationTarget;
  }

  //...
}
Typsichere Messages können auch in EL-Ausdrücken verwendet werden. Hierfür muss das mit @MessageBundle annotierte Interface zusätzlich mit @Named annotiert werden. Listing EL Integration für typsichere Nachrichten zeigt den entsprechenden Teil von UserMessage , welcher mit dem EL-Ausdruck #{userMessage.warning()} angesprochen werden kann.
@Named
@MessageBundle
public interface UserMessage {
  //...

  @MessageTemplate("Warning!")
  String warning();
}
Tipp: Mit @MessageBundle annotierte Interfaces können zusätzlich mit @MessageContextConfig versehen werden. Mit dieser Annotation ist es möglich das Standardverhalten über entsprechende SPI-Implementierungen abzuändern. So kann bspw. eigene Locale-Logik mit einem LocaleResolver umgesetzt werden oder beliebige Message-Quellen referenziert oder ein eigener MessageResolver integriert werden.

 

JSF-Seiten können nicht nur mit typsichere Nachrichten verbessert werden. Auch POST -Requests können mit einer Komponente namens preventDoubleSubmit "sicherer" gemacht werden. Diese Komponente ist im Namespace http://deltaspike.apache.org/jsf verfügbar und stellt sicher, dass ein POST -Request nicht mehrfach gesendet werden kann. Hierfür muss, wie in Listing Verwendung von preventDoubleSubmit , die Komponente in einer JSF-Form eingebettet werden. Im Hintergrund wird ein eindeutiger Request-Token verwendet, welcher serverseitig überprüft wird.
<h:form>
  <!-- ... -->
  <ds:preventDoubleSubmit/>
</h:form>
Tipp: Für Ajax-Requests via POST übernimmt lt. Spezifikation JSF selbst das entsprechende Management. Daher wird für solche Requests keine Überprüfung eines Request-Tokens durchgeführt.

5.6 Bestehendes verbessern

DeltaSpike bereichert CDI und Java EE im Allgemeinen. Darüber hinaus werden bestehende Konzepte verbessert. Ein Beispiel hierfür ist die Annotation @JsfPhaseListener , welche in Listing PhaseListener als CDI-Bean verwendet wird. JSF Phase-Listener können mit dieser Annotation markiert werden, um sie automatisch zu aktivieren. Daher entfällt die sonst übliche Konfiguration in der Datei faces-config.xml . Optional kann eine Priorität via @JsfPhaseListener#ordinal angegeben werden und andere CDI-Beans können in den Phase-Listener injiziert werden.
@JsfPhaseListener
public class DebugPhaseListener implements PhaseListener {
  //...
}
Darüber hinaus kann @JsfPhaseListener mit @Exclude kombiniert werden. Der in Listing PhaseListener bedingt aktivieren dargestellt JSF Phase-Listener wird durch die Verwendung von @Exclude(exceptIfProjectStage = ProjectStage.Development.class) nur aktiviert, wenn der Wert Development für den Project-Stage gesetzt ist.
@JsfPhaseListener
@Exclude(exceptIfProjectStage = ProjectStage.Development.class)
public class DebugPhaseListener implements PhaseListener {
  private static final Logger LOG =
    Logger.getLogger(DebugPhaseListener.class.getName());

  @Override
  public void beforePhase(PhaseEvent event) {
    LOG.info("before " + event.getPhaseId());
  }

  @Override
  public void afterPhase(PhaseEvent event) {
    LOG.info("after " + event.getPhaseId());
  }

  @Override
  public PhaseId getPhaseId() {
    return PhaseId.ANY_PHASE;
  }
}
Ebenfalls verbessert wir das Fenstermanagement, welches seit JSF 2.2 optional aktiviert werden kann. Das Ziel dieser Funktionalität ist die korrekte Behandlung von verschiedenen Browser-Fenstern/Tabs, da dies nicht durch den Session-Scope unterstützt wird. Die abstrakte Klasse javax.faces.lifecycle.ClientWindow wurde auf Basis von Erfahrungen aus Frameworks wie MyFaces CODI definiert und diente als Vorlage für das gleichnamige Interface org.apache.deltaspike.jsf.spi.scope.window.ClientWindow , welches auch mit JSF 2.0 bzw. 2.1 verwendet werden kann. Vor JSF 2.2 muss für ein vollständig korrektes Fenstermanagement die Komponente windowId aus dem Namespace http://deltaspike.apache.org/jsf in jede Seite eingebunden werden. In IdeaFork fügen wir diese Komponente daher am Ende des Templates ein. Listing Fenstermanagement aktivieren zeigt den entscheidenden Ausschnitt aus der Datei main-template.xhtml .
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:ui="http://java.sun.com/jsf/facelets"
      xmlns:ds="http://deltaspike.apache.org/jsf">

<!-- ... -->

<h:body>
    <!-- ... -->
    <ds:windowId/>
</h:body>
</html>
Seit JSF 2.2 kann zwischen dem Standard-Fenstermanagement der JSF Implementierung und dem von DeltaSpike gewählt werden. Wird die ClientWindow -Funktionalität von JSF explizit per Konfiguration aktiviert, dann verwendet DeltaSpike die Window-ID, welche mit javax.faces.lifecycle.ClientWindow#getId abgefragt werden kann und deaktiviert das eigene Fenstermanagement automatisch. Anderenfalls übernimmt DeltaSpike selbst das Fenstermanagement und leitet die Information intern via javax.faces.lifecycle.Lifecycle#attachWindow an JSF weiter, wodurch JSF Implementierungen intern einige Optimierungen bei der Verwaltung vom serverseitigen Zustand der Komponenten durchführen können.

 

Die korrekte serverseitige Zuordnung von Browser-Fenstern/Tabs ist erforderlich, damit DeltaSpike zusätzliche Scopes zur Verfügung stellen kann. Der einfachste dieser Scopes ist der Window-Scope, da dieser mit einer Session je Browser-Fenster/Tab vergleichbar ist. Das JSF-Modul von DeltaSpike aktiviert den zugrundeliegenden Window-Context vor dem Durchlauf des JSF Request-Lifecycles über die Methode org.apache.deltaspike.core.spi.scope.window.WindowContext#activateWindow . Listing Fenstermanagement mit WindowContext verdeutlicht, dass das WindowContext -Interface auch verwendet werden kann um bspw. nach einem Logout Window-scoped Beans mit Hilfe der Methode WindowContext#closeWindow zu zerstören. Anschließend kann der Window-Context für das aktuelle Fenster wieder über die Methode WindowContext#activateWindow aktiviert werden. Hierfür kann die vorherige Window-ID wiederverwendet werden, da diese nach dem Aufruf von WindowContext#closeWindow nicht mehr verwendet wird und somit nicht schlechter als eine neu generierte ID ist. Der Vorteil hierbei ist, dass wir uns nicht um die Aktualisierung der clientseitigen Window-ID kümmern müssen. Je nachdem welche Implementierung von ClientWindow aktiv ist, könnte dies nämlich mitunter sehr aufwändig werden.
@Named("menuBean")
@Model //or just @RequestScoped, since @Named is overruled
public class MenuController {
  @Inject
  private WindowContext windowContext;

  //...

  public Class<? extends ViewConfig> logout() {
    resetWindowContext();
    userHolder.setAuthenticatedUser(null);
    return Pages.User.Login.class;
  }

  //...

  private void resetWindowContext() {
    String currentWindowId = windowContext.getCurrentWindowId();
    windowContext.closeWindow(currentWindowId);
    windowContext.activateWindow(currentWindowId);
  }
}
In IdeaFork müssen wir durch die Aktivierung des Fenstermanagements sicherstellen, dass die Window-ID bei manuellen Aufrufen nicht verloren geht. Einen solchen Aufruf haben wir bspw. in IdeaImportServlet . Listing Explizites Fenstermanagement veranschaulicht, wie die Window-ID manuell weitergereicht werden kann.
@WebServlet("/idea/import")
@MultipartConfig
public class IdeaImportServlet extends HttpServlet {
  @Inject
  private ActiveUserHolder userHolder;

  @Inject
  private FileUploadService fileUploadService;

  @Inject
  private ViewConfigResolver viewConfigResolver;

  @Inject
  private WindowContext windowContext;

  protected void doPost(HttpServletRequest request,
                        HttpServletResponse response)
        throws ServletException, IOException {

    fileUploadService.storeUploadedFiles(
      request.getParts(), userHolder.getAuthenticatedUser());

    ViewConfigDescriptor viewConfigDescriptor = viewConfigResolver
      .getViewConfigDescriptor(Pages.Import.Summary.class);

    request.getRequestDispatcher(
      viewConfigDescriptor.getViewId() +
      "?dswid=" + request.getParameter("dswid"))
      .forward(request, response);
  }
}
Gleiches gilt für unser manuelles Formular auf der Seite upload.xhtml . Bei manuell definierten Formularen und Links müssen wir die aktuelle Window-ID explizit hinzufügen, da dies weder DeltaSpike noch JSF selbst übernehmen kann, wie es bei den äquivalenten JSF-Komponenten umgesetzt ist. In Listing Window-ID in EL-Ausdrücken wird die aktuelle Window-ID mit dem EL-Ausdruck #{dsWindowContext.currentWindowId} im Markup der Seite hinzugefügt.
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
                xmlns:ui="http://java.sun.com/jsf/facelets"
                template="/templates/main-template.xhtml">

  <ui:define name="content-container">
    <div class="panel panel-default">
      <!-- ... -->
      <div class="panel-body">
        <form method="post" enctype="multipart/form-data"
          action="#{jsf.contextPath}/idea/import?dswid=
                  #{dsWindowContext.currentWindowId}">
                 <!-- ... -->
        </form>
      </div>
    </div>
  </ui:define>
</ui:composition>
In IdeaFork haben wir außerdem an einer zweiten Stelle einen selbst erzeugen HTML-Link. Listing Window-ID bei HTML-Links zeigt, dass wir auch diesen mit der aktuellen Window-ID erweitern müssen, damit das Fenstermanagement serverseitig zuverlässig funktioniert.
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
                xmlns:h="http://java.sun.com/jsf/html"
                xmlns:ui="http://java.sun.com/jsf/facelets"
                template="/templates/main-template.xhtml">
  <!-- ... -->

  <ui:define name="content">
    <!-- ... -->
    <a href="#{jsf.contextPath}/public/idea/export/all?dswid=
        #{dsWindowContext.currentWindowId}" class="btn">
      <span class="glyphicon glyphicon-import"/> Export My Ideas
    </a>

    <!-- ... -->
  </ui:define>
</ui:composition>
Tipp: Die explizite Angabe der Window-ID kann entfallen, wenn JSF-Komponenten statt HTML-Tags verwendet werden. Soll dies hingegen bewusst nicht gemacht werden, so muss bspw. eine JSF Command-Komponente in die DeltaSpike-Komponente disableClientWindow eingebettet werden. Der gerenderte Link bzw. Button erhält dadurch keine Window-ID.
Bei welche Beans der Window-Scope sinnvoll ist hängt stark von den konkreten Anforderungen ab. In IdeaFork können wir ein zusätzliches Konzept einführen, um einen möglichen Anwendungsfall zu illustrieren. Bisher wurden auch Navigationen zur vorherigen Seite fix definiert. Dies können wir generisch umsetzen indem wir einen BackNavigator einführen, der die bisherige Navigationshistorie je Browser-Fenster/Tab serverseitig aufzeichnet. Listing Verwendung von @WindowScoped und PreViewConfigNavigateEvent zeigt eine erste Version von BackNavigator , welche mit @WindowScoped annotiert ist. Dies ermöglicht, dass wir mit einem CDI-Observer für das Event PreViewConfigNavigateEvent die Navigationshistorie je Browser-Fenster/Tab speichern können. DeltaSpike feuert dieses Event für jede JSF-Navigation für welche eine typsichere View-Config verwendet wird. In IdeaFork gibt es für jede JSF-Seite bereits eine typsichere Konfiguration, wodurch wir problemlos dieses Event zur Umsetzung des Anwendungsfalles verwenden können. Über dieses Event kann in der Observer-Methode sogar das Navigationsziel verändert werden. In der Methode BackNavigator#onNavigation ist keine Änderung erforderlich, da diese Methode nur die Navigationshistorie aufzeichnen muss. Da wir nicht auf jeder Seite einen expliziten Back-Button haben, können wir die aufgezeichnete Navigationshistorie auf bspw. 10 Einträge beschränken, um ein Speicherleck zu vermeiden. Etwas später werden wir diesen fix definierten Maximalwert durch ein eleganteres Konzept ersetzen.

 

Die Klasse BackNavigator enthält zusätzlich eine zweite Observer-Methode namens #onFacesRequestEnd , welche aufgerufen wird bevor der FacesContext am Ende eines Requests zerstört wird. Dies wird durch den Qualifier org.apache.deltaspike.core.api.lifecycle.Destroyed ermöglicht, welcher hier in Kombination mit FacesContext als Event-Typ verwendet werden kann. Eine solche Observer-Methode ist eine einfache Alternative zu einem @PreDestroy -Callback eines Request-scoped Beans und bietet zusätzlich den Vorteil, dass bei Bedarf noch auf den aktuellen FacesContext zugegriffen werden kann. In unserem Fall setzen wir das Flag backNavigationActive zurück, da dieses sonst bei einem nachfolgenden Request noch den alten Zustand haben könnte.
@Named
@WindowScoped
public class BackNavigator implements Serializable {
  private Stack<Class<? extends ViewConfig>> previousViewStack =
    new Stack<Class<? extends ViewConfig>>();

  private boolean backNavigationActive;

  public Class<? extends ViewConfig> toPreviousView() {
    backNavigationActive = true;
    return previousViewStack.pop();
  }

  protected void onNavigation(
      @Observes PreViewConfigNavigateEvent navigateEvent) {

    Class<? extends ViewConfig> previousView =
      navigateEvent.getFromView();

    if (previousView != null && !this.backNavigationActive &&
       (previousViewStack.isEmpty() ||
        !previousViewStack.peek().equals(previousView))) {

      previousViewStack.push(previousView);

      if (previousViewStack.size() > 10) {
        previousViewStack.remove(0);
      }
    }
  }

  protected void onFacesRequestEnd(
      @Observes(notifyObserver = IF_EXISTS) @Destroyed
      FacesContext facesContext) {

    this.backNavigationActive = false;
  }
}
Tipp: Die Observer-Methode #onFacesRequestEnd selbst wird nur aufgerufen, wenn BackNavigator bereits verwendet wurde. Dies ist nur erforderlich, da BackNavigator im Window-Context abgelegt wird und dieser beim ersten Request in einem Browser-Fenster/Tab nicht aktiv sein muss. Der Grund hierfür ist rein technisch. Abhängig vom konfigurierten Modus für das Fenstermanagement ist es möglich, dass ein initialer Request durch einen Redirect an die gleiche URL abgebrochen wird, damit der angefragten URL die neu erzeugte Window-ID hinzugefügt werden kann. Dies ist bspw. erforderlich, um bei einem Browser-Refresh einer Seite die Window-ID nicht zu verlieren.
In JSF-Seiten kann dieser Mechanismus mit dem EL-Ausdruck #{backNavigator.toPreviousView} verwendet werden. Listing Verwendung von BackNavigator zeigt einen entsprechenden Button, der in IdeaFork auf der Seite profile.xhtml verwendet wird.
<h:commandButton class="btn btn-default" value="Back"
                 action="#{backNavigator.toPreviousView}"/>
Tipp: Der Window-Context speichert Beans gruppiert nach der Window-ID in der aktuellen Session ab. Somit werden Window-scoped Beans ebenfalls repliziert, sobald in einem Cluster Session-Replication durchgeführt wird. Folglich werden Window-scoped Beans auch automatisch zerstört sobald eine Session geschlossen wird.
Eine noch komfortablere Alternative zu @WindowScoped ist ein Scope der auf dem Window-Scope basiert und eine effizientere Speichernutzung ermöglicht. Hierbei handelt es sich um den sog. View-Access Scope. Kurz zusammengefasst existieren @ViewAccessScoped Beans für eine JSF-Seite sobald auf sie zugegriffen wird und werden erst wieder entsorgt wenn nach einer JSF-Navigation auf eine andere Seite nicht mehr auf sie zugegriffen wird. Dies ist sehr ähnlich dem View-Scope von JSF selbst. Der Hauptunterschied liegt darin, dass View-Access-scoped Beans nicht durch die Navigation auf eine andere Seite vor dem Rendering-Prozess zerstört werden. Erst wenn während dem Rendering der neuen Seite nicht mehr auf ein Bean zugegriffen wird, erfolgt die Zerstörung dieser einen Contextual-Instance. Somit kann jede Instanz, die in diesem Context abgelegt ist, eine eigene Lebensdauer haben.

 

Wird bspw. in einem Wizard ein @ViewAccessScoped Bean als Wizard-Controller verwendet und somit bei jedem Wizard-Schritt angesprochen, so steht das @ViewAccessScoped Bean für den gesamten Wizard zur Verfügung und wird automatisch von DeltaSpike entsorgt sobald der Wizard beendet wird und die nachfolgende Seite den Wizard-Controller nicht mehr verwendet. Im Hintergrund sammelt der View-Access Kontext die Contextual-Instances in einem @WindowScoped Bean. Aus diesem Grund werden @ViewAccessScoped Beans per Definition je Browser-Fenster/Tab verwaltet und sobald die Session oder der Window-Context beendet wird, werden auch alle @ViewAccessScoped Beans zerstört.

 

Im vorherigen Schritt haben wir im Seitentemplate von IdeaFork bereits mit der Komponente windowId dafür gesorgt, dass das Fenstermanagement vollständig aktiviert ist. Daher können wir ohne weitere Vorbereitungen die View-Controller Beans in IdeaFork auf den View-Access Scope umstellen. Hierfür stellen wir die Stereotyp-Annotation @ViewController um. Listing Verwendung von @ViewAccessScoped zeigt die neue Implementierung dieser Annotation, in welcher wir statt @RequestScoped die Annotation @ViewAccessScoped verwenden. Da der View-Access-Scope wie der Session-Scope passivierbar ist, müssen wir lt. den CDI-Regeln, die wir in Kapitel CDI Grundkonzepte kennengelernt haben, sämtliche @ViewController Beans mit dem Marker-Interface java.io.Serializable versehen. Außerdem haben wir in IdeaFork bisher einige @ViewController Beans angepasst, indem wir sie explizit als @SessionScoped Beans definiert haben. Dies ist jetzt nicht mehr erforderlich, da wir initial die Lebensdauer dieser View-Controller nur ein wenig ausdehnen wollten.
@Target(TYPE)
@Retention(RUNTIME)

@Stereotype

@ViewAccessScoped
@Named
public @interface ViewController {
}
In IdeaFork verwenden wir bisher zwei Listener für das PreRenderView -Event von JSF, welche mit dem Tag f:event in den Seiten index.xhtml und list.xhtml eingebunden sind. Als typsichere Alternative kann die Annotation @PreRenderView von DeltaSpike für solche Callback-Methoden verwendet werden. Da es im Normalfall mehrere View-Controller in einer Applikation gibt, muss eine Verbindung zwischen einer Seite und dem zuständigen View-Controller definiert werden. Hierfür kann bspw. die Annotation @ViewControllerRef von DeltaSpike verwendet werden. Es ist naheliegend, dass die Konfiguration des View-Controllers ebenfalls in der View-Config der Applikation vorgenommen wird. Listing Verwendung von @ViewControllerRef zeigt einen entsprechend erweiterten Ausschnitt der View-Config von IdeaFork . Als Wert wird die View-Controller-Klasse referenziert.
@View(navigation = REDIRECT)
public interface Pages extends ViewConfig {
  @ViewControllerRef(IndexViewCtrl.class)
  class Index implements Pages {}

  //...

  interface Idea extends SecuredPages {
    //...

    @ViewControllerRef(IdeaListViewCtrl.class)
    class List implements Idea {}
  }

  //...
}
Diese Konfiguration ermöglicht typsichere Callback-Methoden im angegebenen CDI-Bean. Listing Verwendung von @PreRenderView zeigt stellvertretend die Umsetzung in IdeaListViewCtrl , bei welcher die Methode #onPreRenderView mit @PreRenderView annotiert wird. Dies ermöglicht, dass der dazugehörige f:event -Tag aus der JSF-Seite entfernt werden kann. Im Falle von @PreRenderView wird zur Laufzeit vor dem Rendering-Prozess überprüft ob es für die zu rendernde Seite eine View-Config gibt, welche mit @ViewControllerRef annotiert ist. Ist dies der Fall, dann wird die mit @PreRenderView annotierte Methode aufgerufen, sofern eine solche vorhanden ist.
@ViewController
public class IdeaListViewCtrl implements Serializable {
  @Inject
  private IdeaService ideaService;

  @Inject
  private ActiveUserHolder userHolder;

  private List<Idea> ideaList;

  @PreRenderView
  public void onPreRenderView() {
    ideaList = ideaService.loadAllOfAuthor(
      userHolder.getAuthenticatedUser());
  }
  //...
}
Tipp: @PreRenderView ist die gebräuchlichste View-Controller Annotation. Weiters gibt es noch die View-Controller Annotationen @InitView , @PreViewAction und @PostRenderView . @InitView Callback-Methoden werden vor- oder nach einer JSF Request-Lifecycle Phase aufgerufen, wenn die View-ID gesetzt ist bzw. wenn sich der Wert der View-ID geändert hat. @PreViewAction Callback-Methoden werden vor einer Action-Methode ausgeführt, wobei die nachfolgende Action-Methode unabhängig von der konfigurierten Callback-Methode ist. @PostRenderView Callback-Methoden werden aufgerufen sobald der Rendering-Prozess der dazugehörigen Seite beendet ist. So können bspw. Resourcen freigegeben werden ohne die Latenzzeit aus Sicht des Browsers zu erhöhen.
Bisher haben wir einige Aspekte des View-Config-Konzepts kennengelernt, die ohne zusätzlichem Aufwand direkt verwendet werden können. Darüber hinaus erlaubt DeltaSpike eigene View-Config-Metadaten zu definieren, um eigene Konzepte umzusetzen. Eigene Metadaten werden auf gleiche Art und Weise erstellt, wie die bereits verfügbaren Metadaten von DeltaSpike selbst. Der einzige Unterschied liegt in der Auswertung. Während DeltaSpike die zur Verfügung gestellten Annotationen auswertet und die entsprechenden Implementierungen enthält, werden selbst definierte Metadaten auf Basis der gleichen Regeln dem Metadatenmodell hinzugefügt, welches anschließend abgefragt werden kann. Da es sich um eigene Metadaten handelt muss natürlich entsprechende Logik umgesetzt werden, welche die gespeicherten Metadaten verwertet indem die entsprechende Funktionalität aufgerufen wird.

 

In IdeaFork können wir bspw. die eigene Annotation @EntryPoint anlegen. Hierbei müssen grundsätzlich die standard Java-Regeln für Annotationen befolgt werden. Wie in Listing Verwendung von @ViewMetaData zu sehen ist, muss die Annotation @EntryPoint zusätzlich mit der Annotation @ViewMetaData markiert werden, damit DeltaSpike einen entsprechenden Eintrag im ViewConfig -Metadatenmodell erzeugt.
@Target({TYPE})
@Retention(RUNTIME)
@Documented

@ViewMetaData
public @interface EntryPoint {
}
Wie zuvor erwähnt ist es nicht genug eine Annotation anzulegen. Zusätzlich muss noch die dazugehörige Funktionalität umgesetzt werden. Die Annotation @EntryPoint soll in IdeaFork für die Markierung aller Seiten dienen, die eigenständig sind. Daher können gewisse angesammelte Daten zurückgesetzt werden sobald auf solche Seiten navigiert wird. Listing Verwendung von eigenen View-Metadaten zeigt einen Ausschnitt der View-Config von IdeaFork bei welchem einige Seitenkonfigurationen um die Annotation @EntryPoint erweitert wurden.
@View(navigation = REDIRECT)
public interface Pages extends ViewConfig {
  @ViewControllerRef(IndexViewCtrl.class)
  class Index implements Pages {}

  @Secured(UserAwareAccessDecisionVoter.class)
  interface SecuredPages extends Pages {}

  interface User extends Pages {
    @EntryPoint
    class Login extends DefaultErrorView {}

    @EntryPoint
    class Registration implements User {}

    class Profile implements SecuredPages {}
  }

  interface Idea extends SecuredPages {
    @EntryPoint
    class Overview implements Idea {}

    class Create implements Idea {}
    class Edit implements Idea {}

    @ViewControllerRef(IdeaListViewCtrl.class)
    class List implements Idea {}
    class Details implements Idea {}
  }

  //...
}
Die bisherige Implementierung von BackNavigator kann diese neue Marker-Annotation dazu nutzen um die Navigationshistorie zurückzusetzen, statt eine Obergrenze für die Einträge der Navigationshistorie zu definieren. Listing Verwendung von EntryPointNavigationEvent zeigt einen Ausschnitt aus dem erweiterten BackNavigator . In der Methode #onNavigation wird auf die Überprüfung der fix definierten Obergrenze verzichtet. Als Ausgleich gibt es eine Observer-Methode für das neu angelegte (Marker-)Event EntryPointNavigationEvent , in welcher die bisherige Navigationshistorie zurückgesetzt wird und anschließend die aktuelle Seite hinzugefügt wird.
@Named
@WindowScoped
public class BackNavigator implements Serializable {
  //...

  protected void onNavigation(
      @Observes PreViewConfigNavigateEvent navigateEvent) {

    Class<? extends ViewConfig> previousView =
      navigateEvent.getFromView();

    if (previousView != null && !this.backNavigationActive &&
       (previousViewStack.isEmpty() ||
        !previousViewStack.peek().equals(previousView))) {

      previousViewStack.push(previousView);
    }
  }

  protected void onEntryPointNavigation(
      @Observes EntryPointNavigationEvent entryPointNavigationEvent) {

    this.previousViewStack.clear();
    this.previousViewStack.push(entryPointNavigationEvent.getView());
  }

  //...
}
Da EntryPointNavigationEvent ebenfalls ein eigenes Event ist, muss dieses bei der Navigation zu einer mit @EntryPoint markierten Seite erzeugt werden. Daher erstellen wir in IdeaFork eine Klasse namens EntryPointHandler , welche die in Listing Auswertung eigener View-Metadaten gezeigte Observer-Methode #checkEntryPoints enthält. EntryPointNavigationEvent wird wie üblich über das injizierte Interface javax.enterprise.event.Event gefeuert. Damit dieses Event überhaupt gefeuert werden darf, muss überprüft werden ob die Seitenkonfiguration mit @EntryPoint markiert ist. Für solche Auswertungen stellt DeltaSpike das injizierbare Interface ViewConfigResolver zur Verfügung. Der Methode #getViewConfigDescriptor kann die aktuelle View-ID übergeben werden, die der aktuelle FacesContext bzw. der aktuelle View-Root enthält. Sofern es einen View-Config-Eintrag gibt kann mit der Methode ViewConfigDescriptor#getMetaData überprüft werden, ob im ViewConfigDescriptor dieser Seite die Annotation @EntryPoint hinterlegt wurde. Da wir dieses Event nicht feuern wollen, wenn der aktuelle Request von der gleichen Entry-Point Seite kommt, können wir uns die View-Config-Klasse vom letzten Entry-Point merken. Daher ist es naheliegend EntryPointHandler vom Window-Context verwalten zu lassen, weshalb wir die Klasse mit @WindowScoped annotieren.
@WindowScoped
public class EntryPointHandler implements Serializable {
  private Class<? extends ViewConfig> previousEntryPoint;

  @Inject
  private ViewConfigResolver viewConfigResolver;

  @Inject
  private Event<EntryPointNavigationEvent> entryPointEvent;

  protected void checkEntryPoints(
      @Observes @BeforePhase(JsfPhaseId.RENDER_RESPONSE)
      PhaseEvent phaseEvent) {

    UIViewRoot viewRoot = phaseEvent.getFacesContext().getViewRoot();

    if (viewRoot == null) {
      return;
    }
    String viewIdToRender = viewRoot.getViewId();
    ViewConfigDescriptor viewConfigDescriptor =
      viewConfigResolver.getViewConfigDescriptor(viewIdToRender);

    if (viewConfigDescriptor == null) {
      return;
    }

    if (viewConfigDescriptor.getConfigClass()
        .equals(this.previousEntryPoint)) {

      return;
    }

    if (!viewConfigDescriptor
        .getMetaData(EntryPoint.class).isEmpty()) {

      this.previousEntryPoint =
        viewConfigDescriptor.getConfigClass();

      this.entryPointEvent.fire(
        new EntryPointNavigationEvent(
          viewConfigDescriptor.getConfigClass()));
    }
  }
}
Es bedarf aber nicht immer eigener Metadaten, um Anpassungen vorzunehmen. Soll bspw. nur der Namen eines Verzeichnisses oder einer Datei geändert werden, kann dies direkt mit Hilfe von @Folder bzw. @View gemacht werden. Um weitere Funktionalitäten von DeltaSpike zu verwenden erstellen wir einen Wizard, mit welchem eigene Ideen promotet werden können. Hierfür soll ein PromotionRequest erzeugt werden können. Promotion-Requests können durch andere User gesucht und promotet werden. Promotete Ideen sollen anschließend auf der Startseite für alle User sichtbar sein.

 

Der Wizard zur Erstellung eines Promotion-Requests soll aus den Seiten pages/promotion/step1.xhtml , pages/promotion/step2.xhtml und pages/promotion/summary.xhtml bestehen. Listing Explizite Vergabe von Namen veranschaulicht die Anpassung der Verzeichnis- und Dateinamen. Das Konfigurationsinterface für das Verzeichnis des Wizards heißt PromotionWizard . Der Pfad für dieses Verzeichnis lautet jedoch pages/promotion/ statt pages/promotionWizard , da das Interface mit @Folder(name = "promotion") annotiert ist. Die Konfigurationsklasse für die letzte Seite des Wizards heißt in Listing Explizite Vergabe von Namen Pages.PromotionWizard.FinalStep.class und ist mit @View(name = "summary") annotiert, wodurch der Pfad für diese Seite ebenfalls angepasst wird und pages/promotion/summary.xhtml lautet. Pages.PromotionWizard.FinalStep.class implementiert das Interface PromotionWizard , welches SecuredPages erweitert. Da SecuredPages selbst das Interface Pages erweitert, erbt FinalStep über diese Vererbungshierarchie in diesem Fall die Metadaten von Pages. @View(navigation = REDIRECT) wird jedoch nicht von @View(name = "summary") überschrieben, sondern DeltaSpike fügt die Informationen automatisch zusammen, wodurch zur Laufzeit das Ergebnis @View(name = "summary", navigation = REDIRECT) lautet. Informationen werden jedoch nur zusammengeführt, wenn ein Wert nicht explizit angegeben wird. Würden wir FinalStep mit @View(name = "summary", navigation = FORWARD) annotieren, dann würden wir den Navigationsmodus, der ursprünglich durch das Pages -Interface definiert ist, überschreiben.
@View(navigation = REDIRECT)
public interface Pages extends ViewConfig {
  //...

  @Secured(UserAwareAccessDecisionVoter.class)
  interface SecuredPages extends Pages {}

  @Folder(name = "promotion")
  interface PromotionWizard extends SecuredPages {
    @EntryPoint
    @ViewControllerRef(PromotionWizardCtrl.class)
    class Step1 implements PromotionWizard {}

    class Step2 implements PromotionWizard {}

    @View(name = "summary")
    class FinalStep implements PromotionWizard {}
  }
}
Bei Bedarf kann sogar eine komplett eigene Namenskonvention eingeführt werden. JsfBaseConfig definiert neben einigen anderen Konfigurationsoptionen auch Optionen für Default- NameBuilder s wie bspw. JsfBaseConfig.ViewConfigCustomization.CUSTOM_DEFAULT_FOLDER_NAME_BUILDER . Der hinterlegte Konfigurationskey heißt org.apache.deltaspike.jsf.api.config.view.Folder$DefaultFolderNameBuilder und ermöglicht eine eigene Implementierung von org.apache.deltaspike.jsf.api.config.view.Folder$NameBuilder , die über den bereits vorgestellten Konfigurationsmechanismus von DeltaSpike aktiviert und für die gesamte Applikation verwendet werden kann. Alternativ kann die Namensgebung für einen Teil der View-Config angepasst werden. Für Verzeichnisse ist dies durch @Folder#folderNameBuilder möglich. Listing Einzelne Pfade verändern zeigt wie die View-Config von IdeaFork für den zweiten neuen Bereich erweitert werden kann, wenn die Struktur der View-Config nicht der tatsächlichen Verzeichnisstruktur in der Applikation entsprechen soll. Das Interface PromotionSelectionArea ist mit @Folder(folderNameBuilder = PromotionSelectionArea.CustomFolderNameBuilder.class) annotiert, um die Namenskonvention für diese Verzeichniskonfiguration zu ändern. Unser Ziel ist es JSF-Seiten im Verzeichnis /pages/promotion/selection abzulegen. Die Verzeichniskonfiguration wird in Listing Einzelne Pfade verändern allerdings durch Pages.PromotionSelectionArea.class repräsentiert.
@View(navigation = REDIRECT)
public interface Pages extends ViewConfig {
  //...

  @Folder(folderNameBuilder =
    PromotionSelectionArea.CustomFolderNameBuilder.class)
  interface PromotionSelectionArea extends SecuredPages {
    //...

    class CustomFolderNameBuilder
        extends Folder.DefaultFolderNameBuilder {

      //...
    }
  }
}
CustomFolderNameBuilder in Listing Eigener Folder-NameBuilder stellt die einfachste Variante dar, wie ein einzelnes Verzeichnis geändert werden kann. Bevor DeltaSpike die endgültigen Definitionen einer View-Config als unveränderlichen ViewConfigDescriptor speichert, steht die View-Config als veränderbare Node-Struktur zur Verfügung, wobei ViewConfigNode der Typ eines Nodes ist. Wie View-Config Metadaten geändert werden können werden wir etwas später betrachten. Da wir nur den Pfad eines Verzeichnisses verändern wollen, genügt es die Klasse Pages.PromotionSelectionArea.class mit dem Ergebnis der Methode ViewConfigNode#getSource zu vergleichen. Handelt es sich bei der Quelle um die fragliche Konfigurationsklasse, so wird das Flag customPathUsed auf true gesetzt, damit die Methode #isDefaultValueReplaced diese Information später zur Verfügung stellen kann. Außerdem wird der String "/pages/promotion/selection" zurückgegeben. Die Erzeugung der restlichen Verzeichnisnamen wird an DefaultFolderNameBuilder delegiert, wodurch sich das Ergebnis für die anderen Verzeichniskonfigurationen nicht verändert.
class CustomFolderNameBuilder extends Folder.DefaultFolderNameBuilder {
  private boolean customPathUsed = false;

  @Override
  public String build(Folder folder, ViewConfigNode viewConfigNode) {
    if (Pages.PromotionSelectionArea.class
        .equals(viewConfigNode.getSource())) {

      this.customPathUsed = true;
      return "/pages/promotion/selection";
    }
    return super.build(folder, viewConfigNode);
  }

  @Override
  public boolean isDefaultValueReplaced() {
    return super.isDefaultValueReplaced() || this.customPathUsed;
  }
}
Üblicherweise implementieren eigene NameBuilder eigene Namenskonventionen und ersetzen nicht nur einzelne Namen. Solche NameBuilder -Implementierungen werden wie zuvor erwähnt global aktiviert. Eine Umsetzung wie in Listing Angepasste Namen kombinieren kann hingegen sinnvoll sein, wenn eine Applikation noch keiner einheitlichen Namenskonvention folgt und eine entsprechende Umstellung nur Schritt für Schritt durchgeführt wird. NameBuilder -Implementierungen können nicht nur für Verzeichnisse, sondern auch für Dateien angepasst werden. Name-Builder für Verzeichnisse und Dateien sind unabhängig voneinander und daher kann die View-Config wie in Listing Angepasste Namen kombinieren mit @View erweitert werden, um zusätzlich die Namen der Dateien explizit festzulegen.
@View(navigation = REDIRECT)
public interface Pages extends ViewConfig {
  //...

  @Folder(folderNameBuilder =
    PromotionSelectionArea.CustomFolderNameBuilder.class)
  interface PromotionSelectionArea extends SecuredPages {

    @View(name = "list")
    @ViewControllerRef(PromotionRequestListViewCtrl.class)
    class ListPromotions implements PromotionSelectionArea {}

    @View(name = "promote")
    class SelectPromotion implements PromotionSelectionArea {}

    //...
  }
}
Seit JSF 2.0 sind JSF-Actions auch via GET-Requests möglich. Um Request-Parameter automatisch zu übernehmen kann dem Navigationsstring der Marker "includeViewParams=true" hinzugefügt werden. Hierfür stellt @View ebenfalls eine typsichere Konfiguration bereit, welche in Listing Navigationsparameter via View-Config zu sehen ist. Auch Parameter können durch die Verwendung von @NavigationParameter auf Ebene der View-Config angegeben werden. Der Parameter-Wert kann dabei ein fixer String oder eine EL-Expression sein. Als Alternative können Action-Methoden mit @NavigationParameter bzw. mit @NavigationParameter.List annotiert werden, um Parameter auf bestimmte Action-Methoden zu beschränken.
@View(navigation = REDIRECT)
public interface Pages extends ViewConfig {
  //...

  @Folder(folderNameBuilder =
    PromotionSelectionArea.CustomFolderNameBuilder.class)
  interface PromotionSelectionArea extends SecuredPages {

    @View(name = "list", viewParams = INCLUDE)
    @NavigationParameter(key = "searchHint", value = "*")
    @ViewControllerRef(PromotionRequestListViewCtrl.class)
    class ListPromotions implements PromotionSelectionArea {}

    @View(name = "promote")
    class SelectPromotion implements PromotionSelectionArea {}

    //...
  }
}
Tipp: Sollen Parameter dynamisch hinzugefügt werden, dann müssten wir NavigationParameterContext bspw. in einen View-Controller injizieren und dynamisch dessen Methoden aufrufen.
Nach der erfolgreichen Anpassung einzelner Pfadangaben, betrachten wir die Controller der beiden neuen Bereiche genauer. Bei der View-Config für den neuen Wizard haben wir einen View-Controller für Pages.PromotionWizard.Step1.class festgelegt. Der Wizard soll durchgängig von einem Controller namens PromotionWizardCtrl gesteuert werden. Hierfür bietet sich der zuvor vorgestellte View-Access-Scope an. Allerdings wollen wir in unserem Fall die Lebensdauer des Controllers explizit definieren. Am Ende des Wizards soll die Controller-Instanz sofort zerstört werden. Wäre dies die einzige Anforderung, dann könnten wir den Conversation-Scope von CDI selbst verwenden. Allerdings hat dieser einige Einschränkungen und es kann sogar schlimmstenfalls zu unerwarteten BusyConversationException bei Ajax-Requests kommen. Aus diesen und anderen Gründen wurde in DeltaSpike ein eigenes Conversation-Konzept umgesetzt, welches genau wie @WindowScoped und @ViewAccessScoped von MyFaces CODI übernommen wurde. In DeltaSpike wurde der Name der Annotation jedoch umbenannt und somit stehen CODI-Conversations in DeltaSpike unter dem Namen Grouped-Conversations zur Verfügung. Entsprechend heißt die dazugehörige Annotation @GroupedConversationScoped und wird in Listing Verwendung von gruppierten Conversations für PromotionWizardCtrl verwendet.

 

Grouped-Conversations unterscheiden sich in einigen Aspekten von standard CDI-Conversations. Jedes Bean existiert in einer separaten Conversation, welche nicht explizit gestartet werden muss. Im Gegensatz hierzu gibt es beim CDI-Conversation-Scope nur eine große Conversation die explizit gestartet werden muss. Der in Listing Verwendung von gruppierten Conversations gezeigte Ausschnitt von PromotionWizardCtrl zeigt zusätzlich wie die aktuelle Grouped-Conversation des Beans beendet werden kann. GroupedConversation stellt nur die Methode #close zur Verfügung, welche in unserem Fall die aktuelle Instanz von PromotionWizardCtrl sofort aus dem Grouped-Conversation-Context entfernt. Die aktuelle Instanz existiert dann nur noch für die restliche Ausführungszeit der Methode in welcher GroupedConversation#close aufgerufen wurde. Für den nächsten (externen) Methodenaufruf wird eine neue Instanz der Klasse erzeugt und im Grouped-Conversation-Context gespeichert.
@Named
@GroupedConversationScoped
public class PromotionWizardCtrl implements Serializable {
    @Inject
    private GroupedConversation conversation;

    //...

    public Class<? extends Pages> savePromotionRequest() {
        this.ideaService.requestIdeaPromotion(this.promotionRequest);
        this.conversation.close();
        return Pages.Index.class;
    }
}
Da per Default jedes @GroupedConversationScoped Bean in einer isolierten Conversation abgelegt wird, kann es mehrere parallele und unabhängige Conversations geben. Sollte es erforderlich werden alle aktiven Grouped-Conversations zu beenden, dann kann auf GroupedConversationManager zurückgegriffen werden. Listing Verwendung von GroupedConversationManager zeigt eine mögliche Erweiterung von EntryPointHandler , um bei jeder Entry-Point Seite alle Grouped-Conversations zu schließen.
@WindowScoped
public class EntryPointHandler implements Serializable {
  private Class<? extends ViewConfig> previousEntryPoint;

  @Inject
  private ViewConfigResolver viewConfigResolver;

  @Inject
  private GroupedConversationManager conversationManager;

  @Inject
  private Event<EntryPointNavigationEvent> entryPointEvent;

  protected void checkEntryPoints(
      @Observes @BeforePhase(JsfPhaseId.RENDER_RESPONSE)
      PhaseEvent phaseEvent) {

    //...

    if (!viewConfigDescriptor.getMetaData(EntryPoint.class).isEmpty()) {
      this.previousEntryPoint = viewConfigDescriptor.getConfigClass();
      this.conversationManager.closeConversations();
      this.entryPointEvent.fire(
        new EntryPointNavigationEvent(
          viewConfigDescriptor.getConfigClass()));
    }
  }
}
Tipp: Wird ein Bean nicht durch einen expliziten Methodenaufruf zerstört, dann erfolgt dies sobald der Window-Context oder die darunter liegende Session geschlossen wird.
In dem neuen Seitenbereich, der durch Pages.PromotionSelectionArea.class definiert ist, soll man nicht einen Controller für alle Seiten verwenden, sondern jeweils einen eigenen. Wird eine Idee auf der Seite /pages/promotion/selection/promote.xhtml promotet, dann sollen jedoch nicht alle (Grouped-)Conversations beendet werden und auch nicht nur der Controller dieser Seite. Daher können wir für alle Controller-Beans im Seitenbereich Pages.PromotionSelectionArea.class eine Gruppe definieren, wodurch alle Beans einer Gruppe gesammelt beendet werden können. Sollte es noch andere aktive @GroupedConversationScoped Beans geben, die einer anderen Gruppe zugeordnet sind, bleiben diese weiterhin aktiv. Listing Explizite Gruppierung von Conversations zeigt einen Ausschnitt aus den Klassen PromotionRequestListViewCtrl und PromotionRequestSelectionViewCtrl . Beide Klassen sind zusätzlich zu der Annotation @GroupedConversationScoped mit dem CDI-Qualifier @ConversationGroup annotiert, mit welcher die Gruppe typsicher angegeben wird. Hierfür kann eine beliebige Klasse oder ein (Marker-)Interface verwendet werden.
@Named
@GroupedConversationScoped
@ConversationGroup(Pages.PromotionSelectionArea.class)
public class PromotionRequestListViewCtrl implements Serializable {

    //...
}

@Named
@GroupedConversationScoped
@ConversationGroup(Pages.PromotionSelectionArea.class)
public class PromotionRequestSelectionViewCtrl implements Serializable {

    //...
}
Unser Ziel war es alle View-Controller des Seitenbereichs Pages.PromotionSelectionArea.class gesammelt zu beenden. Somit ist es naheliegend dieses Interface auch zur Gruppierung der Beans in Listing Beenden von gruppierten Conversations wiederzuverwenden. Eine Idee wird schließlich durch die Methode PromotionRequestSelectionViewCtrl#promote promotet, in welcher die gesamte (Conversation-)Gruppe durch den Aufruf von GroupedConversationManager#closeConversationGroup beendet wird. Als Parameter wird hier wieder die typsichere Gruppe, in diesem Fall Pages.PromotionSelectionArea.class , verwendet.
Tipp: Rein technisch kann für die Gruppierung von @GroupedConversationScoped -Beans jede beliebige Klasse bzw. jedes Interface verwendet werden. Normalerweise ist es jedoch naheliegend bspw. die typsichere View-Config auch hier zu verwenden.
@Named
@GroupedConversationScoped
@ConversationGroup(Pages.PromotionSelectionArea.class)
public class PromotionRequestSelectionViewCtrl implements Serializable {

  @Inject
  private GroupedConversationManager conversationManager;

  public Class<? extends Pages> promote() {
    conversationManager
      .closeConversationGroup(Pages.PromotionSelectionArea.class);

    ideaService.promoteIdea(this.selectedPromotionRequest);
    return Pages.Index.class;
  }

  //...
}
Der zuvor erstellte Wizard zur Erstellung eines Promotion-Requests funktionieren grundsätzlich. Allerdings wird nicht sichergestellt, dass der Wizard über den festgelegten Entry-Point gestartet wird. Durch Bookmarks oder die manuelle Eingabe der URL kann direkt zu einem beliebigen Wizard-Schritt gesprungen werden. Dadurch kann es zu einem inkonsistenten Zustand in der Applikation kommen. Um dies zu vermeiden können wir eine weitere eigene Annotation für die View-Config erstellen. Listing View-Metadaten verändern zeigt eine mögliche Variante mit dem Namen @Wizard . Im Gegensatz zu @EntryPoint ist diese Annotation nicht nur mit @ViewMetaData markiert, sondern es wird zusätzlich ein ConfigPreProcessor verwendet, um die mit @EntryPoint annotierte Seitenkonfiguration eines Wizards zu finden, sofern der Entry-Point nicht explizit mit Wizard#entryPoint angegeben wird. Damit die nachfolgende Logik nicht unterschiedliche Konstellationen abdecken muss, wird eine neue Instanz von @Wizard mit Hilfe von AnnotationInstanceProvider#of erzeugt und die mit @EntryPoint annotierte Seitenkonfiguration als Wert für Wizard#entryPoint gesetzt. Mit einer eigenen Literal-Klasse für @Wizard kann das gleiche Ergebnis erzielt werden. Durch die Verwendung von AnnotationInstanceProvider ersparen wir uns daher primär die Erstellung einer solchen Literal-Klasse.
@Target({ TYPE })
@Retention(RUNTIME)
@Documented

@ViewMetaData(preProcessor = Wizard.EntryPointProcessor.class)
public @interface Wizard {
  Class<? extends ViewConfig> entryPoint() default ViewConfig.class;

  class EntryPointProcessor implements ConfigPreProcessor<Wizard> {
    @Override
    public Wizard beforeAddToConfig(
        Wizard wizard, ViewConfigNode viewConfigNode) {

      if (!ViewConfig.class.equals(wizard.entryPoint())) {
        return wizard;
      }

      for (ViewConfigNode childNode : viewConfigNode.getChildren()) {
        for (Annotation childMetaData : childNode.getMetaData()) {
          if (EntryPoint.class.equals(childMetaData.annotationType())) {
            Map<String, Object> values = new HashMap<String, Object>();
            values.put("entryPoint", childNode.getSource());

            return AnnotationInstanceProvider.of(Wizard.class, values);
          }
        }
      }
      return wizard;
    }
  }
}
Listing Verwendung änderbarer View-Metadaten zeigt die Verwendung von @Wizard bei dem Interface Pages.PromotionWizard . Die zuvor erwähnte Veränderung der Metadaten führt zur Laufzeit zu der Information @Wizard(entryPoint = Pages.PromotionWizard.Step1.class) , welche durch die Metadaten-Vererbung an Pages.PromotionWizard.Step1.class , Pages.PromotionWizard.Step2.class und Pages.PromotionWizard.FinalStep.class vererbt wird.
@View(navigation = REDIRECT)
public interface Pages extends ViewConfig {
  //...

  @Folder(name = "promotion")
  @Wizard
  interface PromotionWizard extends SecuredPages {
    @EntryPoint
    @ViewControllerRef(PromotionWizardCtrl.class)
    class Step1 implements PromotionWizard {}

    class Step2 implements PromotionWizard {}

    @View(name = "summary")
    class FinalStep implements PromotionWizard {}
  }
}
Wie bei @EntryPoint muss auch @Wizard an der entsprechenden Stelle ausgewertet werden. Dies kann ebenfalls in der Klasse EntryPointHandler umgesetzt werden. Die ursprüngliche Methode #checkEntryPoints wird in Listing Kombinierte Auswertung eigener Metadaten #checkEntryPointsAndWizardSteps genannt und um einen zusätzlichen Block erweitert in welchem bei einer vorhandenen @Wizard Annotation für die aktuelle Seite überprüft wird, ob der Wizard ursprünglich über den referenzierten Entry-Point betreten wurde. Ist dies nicht der Fall, dann wird über den injizierten ViewNavigationHandler eine Navigation an diesen Entry-Point durchgeführt. Somit kann es zu keinem inkonsistenten Zustand in der Applikation kommen, da der Wizard immer über den festgelegten Entry-Point betreten wird.
@WindowScoped
public class EntryPointHandler implements Serializable {
  private Class<? extends ViewConfig> previousEntryPoint;

  @Inject
  private ViewConfigResolver viewConfigResolver;

  @Inject
  private ViewNavigationHandler viewNavigationHandler;

  @Inject
  private GroupedConversationManager conversationManager;

  @Inject
  private Event<EntryPointNavigationEvent> entryPointEvent;

  protected void checkEntryPointsAndWizardSteps(
      @Observes @BeforePhase(JsfPhaseId.RENDER_RESPONSE)
      PhaseEvent phaseEvent) {

    //...

    if (!viewConfigDescriptor.getMetaData(EntryPoint.class).isEmpty()) {
      this.previousEntryPoint = viewConfigDescriptor.getConfigClass();
      this.conversationManager.closeConversations();
      this.entryPointEvent.fire(
        new EntryPointNavigationEvent(
          viewConfigDescriptor.getConfigClass()));
    } else if (!viewConfigDescriptor
                  .getMetaData(Wizard.class).isEmpty()) {

      Wizard wizard =
        viewConfigDescriptor.getMetaData(Wizard.class).iterator().next();

      Class<? extends ViewConfig> entryPointOfWizard =
        wizard.entryPoint();

      if (!entryPointOfWizard.equals(this.previousEntryPoint)) {
        this.viewNavigationHandler.navigateTo(entryPointOfWizard);
      }
    }
  }
}
Die bisher implementierte Logik für @Wizard stellt jedoch nicht sicher, dass ein Wizard auch wirklich einen definierten Entry-Point besitzt. Um dies während des Applikationsstartes zu überprüfen, kann ein ConfigDescriptorValidator implementiert werden und wie in Listing Verwendung von @ViewConfigRoot via @ViewConfigRoot aktiviert werden.
@ViewConfigRoot(
  configDescriptorValidators = IdeaForkViewMetaDataValidator.class)
@View(navigation = REDIRECT)
public interface Pages extends ViewConfig {
  //...

  @Folder(name = "promotion")
  @Wizard
  interface PromotionWizard extends SecuredPages {
    @EntryPoint
    @ViewControllerRef(PromotionWizardCtrl.class)
    class Step1 implements PromotionWizard {}

    class Step2 implements PromotionWizard {}

    @View(name = "summary")
    class FinalStep implements PromotionWizard {}
  }

  //...
}
Implementierungen des Interfaces ConfigDescriptorValidator können wie in Listing Implementierung von ConfigDescriptorValidator logische Zusammenhänge von View-Config-Metadaten validieren. In IdeaForkViewMetaDataValidator wird validiert, ob je ViewConfigDescriptor -Instanz maximal eine @Wizard -Instanz existiert und der Default-Wert für Wizard#entryPoint gegen einen konkreten Entry-Point ersetzt wurde. Entweder durch die explizite Angabe mit der @Wizard Annotation selbst oder durch die Kombination mit @EntryPoint , die wir anfänglich in Wizard$EntryPointProcessor umgesetzt haben.
public class IdeaForkViewMetaDataValidator
    implements ConfigDescriptorValidator {

  @Override
  public boolean isValid(ConfigDescriptor configDescriptor) {
    List<Wizard> wizardMetaData =
      configDescriptor.getMetaData(Wizard.class);

    if (wizardMetaData.isEmpty()) {
      return true;
    }

    if (wizardMetaData.size() > 1) {
      throw new IllegalStateException("...");
    }

    Wizard wizardAnnotation = wizardMetaData.iterator().next();

    if (ViewConfig.class.equals(wizardAnnotation.entryPoint())) {
      throw new IllegalStateException("...");
    }

    return true;
  }
}
Tipp: Mit @ViewConfigRoot kann nicht nur das View-Config Konzept erweitert oder angepasst werden, sondern in Kombination mit bspw. @ApplicationScoped ist diese Annotation erforderlich wenn der seit CDI 1.1 verfügbare bean-discovery-mode annotated aktiviert ist.
In diesem Kapitel haben wir unter anderem einige View-Config Mechanismen kennengelernt und wie diese mit anderen Funktionalitäten von DeltaSpike kombiniert werden können. Darüber hinaus stehen noch weitere Anpassungsmöglichkeiten und Annotationen zur Verfügung. So kann bspw. mit @ViewRef die Konfiguration von View-Controller dezentralisiert werden. Im nachfolgenden Teil werden wir uns die Integration mit anderen Bibliotheken, alternative Konzepte für Java EE Mechanismen und die frühzeitige Verwendung von EE7 Funktionalitäten in einer EE6 Applikation näher ansehen.

5.7 Flexibilität mit Alternativen

Bisher haben wir DeltaSpike in diesem Kapitel primär zur Erweiterung von standard CDI-Konzepten bzw. von anderen Java EE Spezifikationen wie bspw. JSF verwendet. Abgesehen von solchen Erweiterungen stellt DeltaSpike auch Alternativen zu bestehenden Java EE Konzepten zur Verfügung. Einige Alternativen, wie bspw. typsichere Project-Stages und die typsichere JSF-Navigation, haben wir bereits kennengelernt. In diesem Abschnitt geht es jedoch um Alternativen, welche jeweils als eigenes Modul von DeltaSpike verfügbar sind. Der Hauptunterschied zu den äquivalenten Mechanismen von Java EE ist die höhere Flexibilität. Sämtliche Alternativen können bspw. auch außerhalb von Java EE Servern eingesetzt werden. Außerdem ergeben sich durch zusätzliche Erweiterungspunkte neue Möglichkeiten das Standardverhalten zu erweitern bzw. vollständig zu ändern.

 

Wir beginnen mit der Automatisierung eines Bereichs in IdeaFork , den wir in diesem Kapitel neu hinzugefügt haben. Bisher kann unser Config-Context nur manuell über JMX zurückgesetzt werden, um evt. geänderte Werte aus den Konfigurationsquellen erneut einzulesen. Zusätzlich zu dieser Möglichkeit können wir in regelmäßigen Intervallen einen automatischen Reset der Beans im Config-Context durchführen, damit die entsprechenden CDI-Beans und somit die geladenen Konfigurationswerte regelmäßig aktualisiert werden.
In Java EE würden wir hierfür die EJB Annotation @javax.ejb.Schedule verwenden. Der Vorteil dieser Annotation ist eine hohe Portabilität zwischen Java EE Servern. Sobald wir eine Applikation außerhalb eines Java EE Servers deployen wollen, müssten wir einen zusätzlichen Container wie bspw. die Embedded Version von Apache OpenEJB verwenden. Alternativ ermöglicht das Scheduler-Modul von DeltaSpike die Verwendung von Quartz-Jobs als CDI-Beans. Listing Quartz-Job als CDI-Bean veranschaulicht die Verwendung der Annotation @org.apache.deltaspike.scheduler.api.Scheduled . In diesem Beispiel ist die Klasse ConfigReloaderJob eine Implementierung von org.quartz.Job . Durch die Annotation @Scheduled wird dieser Quartz-Job automatisch aktiviert und CDI-basierte Injizierung verfügbar. Aus diesem Grund ist keine zusätzliche Konfiguration erforderlich und das ConfigReloader -Bean kann wie gewohnt einfach injiziert werden. Die Angabe eines CDI-Scopes für den Quartz-Job ist optional. Grundsätzlich kann jeder aktive Scope gewählt werden.
@ApplicationScoped
@Scheduled(cronExpression = "0 0/10 * * * ?")
public class ConfigReloaderJob implements Job {
  @Inject
  private ConfigReloader configReloader;

  @Override
  public void execute(JobExecutionContext context)
    throws JobExecutionException {
      configReloader.reloadConfig();
  }
}
Neben der Steuerung der Ausführungszeitpunkte mit Hilfe einer CRON-Expression kann auch die Context-Steuerung angepasst werden. Der Default-Wert für @Scheduled#startScopes ist SessionScoped.class und RequestScoped.class . Ohne einer expliziten Angabe anderer Scope-Annotationen, wird im Hintergrund für jeden gestarteten Quartz-Job automatisch der Request- und Session-Scope gestartet und nach der Ausführung der #execute -Methode wieder beendet. Gestartet und gestoppt werden die angegebenen Contexte hierbei mithilfe von DeltaSpike CDI-Control, welches wir bei der Verwendung vom Test-Control-Modul noch im Detail kennenlernen werden.

 

Die explizite Steuerung des Schedulers und einzelner Scheduler-Jobs kann optional über das org.apache.deltaspike.scheduler.spi.Scheduler -SPI umgesetzt werden. So ist es bspw. möglich mit @Scheduled(onStartup = false) die automatische Konfiguration eines Scheduling-Jobs zu deaktivieren, um bspw. eine Contextual-Reference auf Scheduler in ein beliebiges CDI-Bean zu injizieren und einen solchen Scheduling-Job abhängig von einer Konfiguration manuell via Scheduler#startJobManually auszuführen.
Tipp: Durch das Scheduler -SPI kann selbst Quartz als Scheduling-Framework ersetzt werden. Sofern das favorisierte Scheduling-Framework mindestens Scheduling-Jobs auf Basis von CRON-Expressions unterstützt, kann eine Implementierung von org.apache.deltaspike.scheduler.spi.Scheduler als Adapter zu diesem Scheduling-Framework dienen.

 

 

Ein weiteres Modul, welches primär als CDI-basierte Alternative zu EJBs geschaffen wurde, ist das JPA-Modul von DeltaSpike. Die Annotationen @org.apache.deltaspike.jpa.api.transaction.Transactional , @org.apache.deltaspike.jpa.api.transaction.TransactionScoped und @PersistenceUnitName stellen die zentralen Bestandteile dieses Moduls dar. Für die ersten beiden Annotationen gibt es seit Java EE 7 (bzw. JTA v1.2) gleichnamige Äquivalente, wobei beide @Transactional -Varianten zwar grundsätzlich das gleiche Ziel haben, dieses jedoch im Detail anders umsetzen.

 

In IdeaFork werden Transaktionen bisher implizit durch EJBs auf Service-Ebene gesteuert. Zusammengefasst wird eine Proxy-Instanz für den EntityManager in der Klasse EntityManagerProducer vom EE-Server injiziert. Durch die selbst definierte CDI Producer-Methode können wir diesen EntityManager in EJBs bzw. CDI-Beans typsicher injizieren. Im Hintergrund erzeugt der EJB-Container je Transaktion eine neue EntityManager -Instanz.

 

Listing EntityManager-Producer unabhängig von EJBs zeigt wie wir mit dem JPA-Modul von DeltaSpike das gleiche Verhalten ohne EJBs umsetzen können.
@ApplicationScoped
public class EntityManagerProducer {
  @PersistenceUnit(unitName = "ideaForkPU")
  private EntityManagerFactory entityManagerFactory;

  @Produces
  @Default
  @TransactionScoped
  protected EntityManager exposeEntityManagerProxy() {
    return entityManagerFactory.createEntityManager();
  }

  protected void onTransactionEnd(
    @Disposes @Default EntityManager entityManager) {
      if (entityManager.isOpen()) {
        entityManager.close();
      }
  }
}
Da wir auf den EJB-Support verzichten, müssen wir auf die threadsichere EntityManagerFactory ausweichen. In einem EE-Server wird EntityManagerFactory in Kombination mit @PersistenceUnit durch den Container selbst injiziert. Außerhalb eines EE-Servers müssten wir auf Plug-ins für den CDI-Container zurückgreifen. OpenWebBeans stellt bspw. mit dem Resource-Modul ein entsprechendes Plugin zur Verfügung. Dieses ist jedoch nicht portabel und somit nur mit OpenWebBeans verwendbar. Ein portables Ergebnis können wir außerhalb des EE-Servers mit der Annotation @org.apache.deltaspike.jpa.api.entitymanager.PersistenceUnitName erzielen.

 

Bevor wir diese Annotation verwenden betrachten wir das bisherige Vorgehen bei unseren Tests. In IdeaFork verwenden wir für Unit-Tests In-Memory-Repositories. Ein solches Vorgehen wird oft empfohlen, wenn nicht die Repositories selbst getestet werden. Werden allerdings zu viele zentrale Klassen für Tests ausgetauscht, können Fehler oft nicht früh genug erkannt werden. In den Tests von IdeaFork haben wir dies in der Test-Klasse EventTest symbolisch illustriert. Konkret rufen wir in verschiedenen Test-Methoden die Methode UserManager#createUserFor auf. Danach werden Ideen für diesen User erzeugt und gespeichert. Durch die In-Memory-Repositories im Test-Code ist jedoch nicht aufgefallen, dass die erzeugte User-Instanz nicht gespeichert wurde. Dies fällt erst auf, wenn wir auch in den Tests die produktiven Repository-Implementierungen verwenden. Um auch in Tests unsere normalen JPA-Repositories beizubehalten, löschen wir einfach die spezialisierten Repository-Implementierungen, wodurch die produktiven Implementierungen wieder automatisch aktiv werden. Wie zuvor erwähnt kann der in Listing EntityManager-Producer unabhängig von EJBs beschriebene CDI-Producer für den EntityManager nicht portabel außerhalb eines EE-Servers eingesetzt werden. Stattdessen können wir im Testmodul die in Listing Portabler Test-EntityManager-Producer ersichtliche spezialisierte Variante der EntityManagerProducer -Klasse einführen. Durch den Einsatz der Qualifier-Annotation @PersistenceUnitName können wir weiterhin den Injection-Point für EntityManagerFactory automatisch befüllen lassen. Die restliche Producer-Logik ist äquivalent zu EntityManagerProducer .
@Specializes
public class TestEntityManagerProducer extends EntityManagerProducer {
  @Inject
  @PersistenceUnitName("ideaForkPU")
  private EntityManagerFactory entityManagerFactory;

  //...
}
Die entsprechenden Änderungen sind im Git-Repository von IdeaFork in einem Commit zusammengefasst. Dieser Commit zeigt zusätzlich, dass weitere Test-Dependencies für JPA in Unit-Tests und eine eigene JPA-Konfiguration (siehe META-INF/persistence.xml ) erforderlich sind. Beides ist unabhängig von CDI bzw. DeltaSpike und aus diesem Grund gehen wir auf diese Details nicht näher ein.

 

An diesem Punkt haben wir den EntityManagerProducer umgestellt und die erzeugte EntityManager -Instanz wird durch @TransactionScoped im Transaction-Kontext für den aktuellen Thread abgelegt. Der Transaction-Kontext ist jedoch nicht automatisch aktiv, sondern wird durch den @Transactional -Interceptor von DeltaSpike aktiviert, da dieser Interceptor die Transaktionen steuert. Aus diesem Grund kann @TransactionScoped nur in Verbindung mit @Transactional eingesetzt werden.

 

In IdeaFork haben wir die Transaktionsbehandlung bisher im EE6-Modul durch EJBs definiert. Im Core von IdeaFork hatten wir somit keine transaktionalen Beans. Unsere Umstellung von In-Memory-Repositories auf JPA-Repositories erfordert daher transaktionale Beans in IdeaFork -Core. Listing Erweiterung eines Stereotyps zeigt die erforderliche Erweiterung der @Repository -Stereotype-Annotation. Sobald wir die @Transactional -Annotation in unserer @Repository -Annotation hinzufügen sind alle Repository-Implementierungen in IdeaFork transaktional.
@Target(TYPE)
@Retention(RUNTIME)

@Stereotype
@ApplicationScoped
@Monitored

@Transactional
public @interface Repository {
}
Dies setzt allerdings CDI 1.1 bzw. Java EE7 voraus. Mit CDI 1.0 und dadurch auch mit EE6 müssen wir den Interceptor noch in der Datei META-INF/beans.xml angeben. In manchen EE6-Servern, wie bspw. Apache TomEE, kann darauf bereits verzichtet werden und in anderen sind die BDA-Regeln so strikt umgesetzt, dass der Interceptor in jedem CDI-Archiv erneut aktiviert werden muss. In solchen Fällen muss die Klasse org.apache.deltaspike.jpa.impl.transaction.TransactionalInterceptor als Interceptor-Klasse, wie es in Listing Aktivierung von TransactionalInterceptor für EE6-Server ersichtlich ist, hinzugefügt werden. Ohne diese Aktivierung würden die Annotation @Transactional einfach ignoriert werden.
Listing Aktivierung von TransactionalInterceptor für EE6-Server zeigt den erforderlichen Konfigurationseintrag.
<beans>
  <!-- ... -->

  <interceptors>
    <!-- ... -->
    <class>
      org.apache.deltaspike.jpa.impl.transaction.TransactionalInterceptor
    </class>
  </interceptors>

  <!-- ... -->
</beans>
Bei den Service-Implementierungen des EE6-Moduls von IdeaFork können wir einen ähnlichen Weg gehen. Allerdings müssen wir hier erst eine Stereotype-Annotation anlegen. Listing Stereotype für transaktionale Services veranschaulicht diese Stereotype-Annotation namens @Service .
@Target(TYPE)
@Retention(RUNTIME)

@Stereotype
@ApplicationScoped

@Transactional
public @interface Service {
}
Beans welche mit diesem neuen @Service -Stereotype markiert sind, werden automatisch zu transaktionalen application-scoped Beans. @Transactional können wir entweder auf Klassenebene oder auf Methodenebene verwenden. Durch die Verwendung von @Transactional in den beiden Stereotype-Annotationen @Repository und @Service , werden alle Methoden des annotierten CDI-Beans transaktional. Würden wir stattdessen einzelne Methoden explizit mit @Transactional annotieren, dann würden nur diese Methoden in einer Transaktion ausgeführt.

 

In IdeaFork sind sowohl Repository-Beans als auch Service-Beans transaktional, damit Repository-Beans fein-granular und ohne zusätzliche Konstrukte getestet werden können bzw. IdeaFork -Core als eigenständiges Modul verwendbar ist. Im EE6-Modul optimieren wir die Transaktionsgrenzen für die JSF-Applikation durch die Definition von transaktionalen Service-Beans. Wir könnten somit neben dem EE6-Modul auch Module für andere UI-Technologien umsetzen, welche bspw. andere Anforderungen an Transaktionsgrenzen haben. In jedem der möglichen Fälle stellt IdeaFork -Core sicher, dass zumindest Repository-Beans im Kontext einer aktiven Transaktion ausgeführt werden. Beim Aufruf einer transaktionalen Methode überprüft DeltaSpike automatisch, ob für den aktuellen Thread bereits eine Transaktion aktiv ist. Ruft eine transaktionale Service-Methode eine oder mehrere transaktionale Repository-Methode/n auf, so wird nur eine Transaktion auf Service-Ebene gestartet und nach dem Aufruf wieder beendet. Alle Repository-Aufrufe werden in dieser Konstellation im Transaktionskontext der Service-Methode ausgeführt und erhalten daher keine eigene Transaktion. Werden transaktionale Repository-Methoden hingegen von einem nicht-transaktionalen (CDI-)Bean aufgerufen, dann wird jede Repository-Methode in einer eigenen Transaktion ausgeführt. Daraus resultiert, dass eine Transaktion immer durch die äußerste transaktionale Methode gestartet und gestoppt wird. Das Ergebnis ähnelt stark den Konzepten die durch EJBs definiert werden. Ein großer Unterschied besteht darin, dass transaktionale CDI-Beans explizit mit @Transactional markiert werden müssen. Außerdem ist die Transaktionsstrategie von DeltaSpike bei Bedarf anpassbar.
Mit diesen Informationen können wir unsere bisherigen EJBs auf transaktionale CDI-Beans umstellen. Listing Transaktionale CDI-Beans statt EJBs veranschaulicht diese Umstellung stellvertretend für IdeaService .
//old
@Stateless
public class IdeaService {
  @Inject
  private IdeaManager ideaManager;

  //...
}

//new
@Service
public class IdeaService {
  @Inject
  private IdeaManager ideaManager;

  //...
}
Auch möglicherweise aufgetretene Exceptions werden auf dieser obersten Ebene behandelt. Wird eine transaktionale Methode von einer anderen transaktionalen Methode aufgerufen, dann wird die Transaktion erst zurückgerollt wenn die äußerste transaktionale Methode eine Exception nicht fängt und behandelt bzw. selbst wirft.

 

Der Commit im Git-Repository enthält auch eine Änderung in der Klasse AppStructureValidationExtension . Diese Änderung ist erforderlich, da die Service-Implementierungen in IdeaFork keine EJBs mehr sind, sondern mittlerweile mit @Service annotiert werden sollen.

 

Bei der Verwaltung von Transaktionen stellt DeltaSpike mehrere Varianten bereit und ermöglicht zusätzlich die Umsetzung eigener Konzepte. Dies ist durch das org.apache.deltaspike.jpa.spi.transaction.TransactionStrategy -SPI möglich. In TransactionalInterceptor wird ein CDI-Bean injiziert, welches dieses Interface implementiert. Standardmäßig ist eine Implementierung namens ResourceLocalTransactionStrategy aktiv, welche für Persistence-Units "RESOURCE_LOCAL" als Wert für "transaction-type" voraussetzt. Aus diesem Grund kann @Transactional innerhalb und außerhalb von Java EE Servern eingesetzt werden. Darüber hinaus stellt DeltaSpike zwei weitere Implementierungen des TransactionStrategy -Interfaces zur Verfügung. BeanManagedUserTransactionStrategy ermöglicht die Verwendung von "JTA" als Wert für "transaction-type" und EnvironmentAwareTransactionStrategy unterstützt beide Konfigurationen indem für jede aktive Transaktion der Transaktionstyp ermittelt wird.

 

Soll die aktive Implementierung geändert werden, so kann die gewünschte alternative Klasse in der Datei META-INF/beans.xml konfiguriert werden. Vor allem bei Weld-basierten EE6-Servern ist dies durch die striktere Auslegung der BDA-Regeln nicht möglich. Um bei solchen Servern dennoch eine alternative Implementierung global zu aktivieren, können wir auf das Global-Alternative Konzept von DeltaSpike zurückgreifen. In unserem Fall legen wir hierfür die Datei META-INF/apache-deltaspike.properties an und fügen die Zeile aus Listing Globale Alternativen mit CDI 1.0 hinzu.
globalAlternatives.org.apache.deltaspike.jpa
.spi.transaction.TransactionStrategy=org.apache.deltaspike.jpa
.impl.transaction.EnvironmentAwareTransactionStrategy
Diese Konfigurationsdatei ist wie jede andere Konfigurationsquelle im Konfigurationsmechanismus von DeltaSpike eingebunden und wird intern durch die Klasse PropertyFileConfigSource geladen. In diesem Kapitel haben wir bereits die Priorisierung von Konfigurationsquellen kennengelernt. PropertyFileConfigSource verwendet standardmäßig den Wert 100 für Konfigurationsdateien mit dem Namen META-INF/apache-deltaspike.properties . Gleiches gilt für Konfigurationsdateien, die durch Implementierungen des Interfaces PropertyFileConfig automatisch eingebunden werden. Jede Konfigurationsquelle, so auch jede Datei mit dem Namen META-INF/apache-deltaspike.properties , kann die vordefinierte Priorität selbst festlegen. Hierfür müssen wir einen Konfigurationseintrag mit dem Key deltaspike_ordinal hinzufügen. Als Wert wählen wir den gewünschten Platz in der Konfigurationskette. DeltaSpike verwendet für eigene Konfigurationsquellen die Werte 100, 200, 300 und 400. Möchten wir sicherstellen, dass unsere eben definierte Konfiguration für die globale Transaction-Strategy immer Vorrang vor Werten in den Standardquellen hat, dann müssen wir einen höheren Wert als 400 wählen. In unserem Fall fügen wird in der Datei META-INF/apache-deltaspike.properties den Konfigurationseintrag deltaspike_ordinal=1000 hinzu.
Tipp: Die Annotation @Transactional ist zwar als Alternative zu EJBs gedacht, aber kann auch mit diesen kombiniert werden. Verwendet @Transactional die aktuelle UserTransaction und somit JTA, dann kann ein transaktionales CDI-Bean die UserTransaction eines EJBs übernehmen, sofern das EJB eine transaktionale Methode eines CDI-Beans aufruft.

5.8 Eigene Konzepte

In IdeaFork haben wir bisher mehrere Zugriffsmöglichkeiten auf konfigurierte Werte kennengelernt, welche auf Basis von DeltaSpike umgesetzt wurden. Die gezeigten Möglichkeiten veranschaulichen einige Konzepte welche durch DeltaSpike direkt unterstützt werden. Jede der beschriebenen Varianten hatte zumindest einen Aspekt, der verbesserungswürdig ist. In vielen Fällen liegt dies in der Natur der Sache, da wir möglichst typsicher auf untypisierte Konfigurationswerte zugreifen wollen. So erfordert die Verwendung von @ConfigProperty , dass der Key bei jedem Injection-Point angegeben werden und bei Änderungen aktualisiert werden muss. Hier könnten wir natürlich bspw. auf ein Interface zurückgreifen, welches die Keys als statische Strings definiert. Dennoch muss bei der Implementierung eine entsprechend hohe Disziplin eingehalten werden. Bei der zweiten Variante, den Producer-Klassen, die von BaseConfigPropertyProducer ableiten, benötigen wir je Key eine eigene Producer-Methode. Solche Methoden sind zwar trivial in der Umsetzung, dennoch ist für jeden Key eine Implementierung und in vielen Fällen auch ein CDI-Qualifier nötig. Ähnlich sieht es bei eigenen Konfigurationsbeans aus. Der Vorteil von MonitoringConfig war die optionale Verwendung eines beliebigen Scopes, dennoch mussten wir je Konfigurationskey eine eigene Methode implementieren.

 

Die eben erwähnten Einschränkungen können wir jedoch in wenigen Schritten durch die Umsetzung eines eigenen Konzepts überwinden. Um dies zu bewerkstelligen können wir uns dem Partial-Bean Modul von DeltaSpike bedienen. Dieses Modul erlaubt die Umsetzung von Interfaces und abstrakten Klassen, für welche es nur einen generischen Handler statt konkreter Implementierungen gibt. Die Verbindung von Interfaces bzw. abstrakten Klassen mit dem dazugehörigen generischen Handler wird über eine selbst definierte Binding-Annotation durchgeführt. Eine solche Binding-Annotation ist in Listing Partial-Bean Binding zu sehen und muss mit der Annotation @org.apache.deltaspike.partialbean.api.PartialBeanBinding markiert werden.
@PartialBeanBinding

@Retention(RUNTIME)
@Target(TYPE)
public @interface TypedConfig {
}
Sowohl Interfaces bzw. abstrakte Klassen als auch die Handler-Klasse muss mit der gleichen Binding-Annotation versehen werden, um eine Verbindung herzustellen. In IdeaFork haben wir in diesem Kapitel den Qualifier @MaxNumberOfHighestRatedCategories eingeführt, um den konfigurierten Wert typsicher zu injizieren. Ein Qualifier je Konfigurationskey verbessert zwar die Sicherheit bei der Verwendung, aber erhöht zugleich den Aufwand bei der initialen Umsetzung. Vor diesem Qualifier hatten wir eine Klasse namens ApplicationConfig , welche mehrere Konfigurationswerte typsicher bereitstellte. Allerdings mussten wir die Konfiguration selbst laden und die einzelnen Properties aufbereiten bzw. bei mehreren Konfigurationsquellen wäre sogar eine Priorisierung je Quelle nötig. Das Ergebnis haben wir unabhängig von den vorgelagerten Schritten in diesem Fall über die Methode #getMethodInvocationThreshold konsumiert. Abgesehen von der manuellen Verarbeitung der Konfiguration war dieser Ansatz durchaus praktisch. Daher wollen wir unsere ursprüngliche Herangehensweise mit dem Partial-Bean Konzept auf Basis der Konfigurationsinfrastruktur von DeltaSpike etwas verfeinern.

 

Als erste Vereinfachung entfernen wir den Qualifier @MaxNumberOfHighestRatedCategories wieder und legen ApplicationConfig diesmal als Interface mit der Methode #maxNumberOfHighestRatedCategories an. Das in Listing Minimales Partial-Bean gezeigte Interface verbinden wir durch die @TypedConfig -Annotation mit einem generischen Handler. Als Ergebnis erhalten wir unser erstes Partial-Bean.
@TypedConfig
public interface ApplicationConfig {
  Integer maxNumberOfHighestRatedCategories();
}
Bisher haben wir eine Binding-Annotation und ein Interface erstellt. Der zuvor erwähnte generische Handler wird im nächsten Schritt realisiert. Dieser Handler muss java.lang.reflect.InvocationHandler implementieren und mit unserer Binding-Annotation versehen werden. In Listing Minimaler Partial-Bean Handler ist eine vereinfachte Variante eines Handlers dargestellt. Der Methodenname wird als Key der bereits vorgestellten Methode ConfigResolver#getPropertyValue übergeben. Anschließend wird der von DeltaSpike geladene Wert auf Basis des Return-Typs der Methode geparsed. Dieses Ergebnis stellt folglich das Resultat der Partial-Bean-Methode dar.
@TypedConfig
public class TypedConfigHandler implements InvocationHandler {
  public Object invoke(Object proxy, Method method, Object[] args)
    throws Throwable {

      String key = method.getName();
      Class<?> configType = method.getReturnType();

      String loadedValue = ConfigResolver.getPropertyValue(key);
      return parseValue(loadedValue, configType);
  }

  private Object parseValue(String loadedValue, Class<?> configType) {
    //...
  }
}
Ohne zusätzliche Konfiguration oder Aktivierung ist unser erstes Partial-Beans sofort einsatzbereit und wir können ApplicationConfig wie ein herkömmliches CDI-Bean in andere CDI-Beans injizieren. Bei jedem Methodenaufruf, in unserem Fall von der Methode #maxNumberOfHighestRatedCategories , wird im Hintergrund der generische Handler für dieses Partial-Bean aufgerufen.
Listing Verwendung von Partial-Beans veranschaulicht, dass bei der Verwendung eines Partial-Beans keine weitere Aspekte berücksichtigt werden müssen. Alleine durch die Betrachtung der Verwendung bspw. in IdeaJpaRepository können wir keinen Unterschied zu einem herkömmlichen CDI-Bean feststellen. Erst bei der Suche nach der dazugehörigen Implementierung würden wir herausfinden, dass keine explizite Implementierung des Interfaces vorhanden ist.
@Repository
public class IdeaJpaRepository extends GenericJpaRepository<Idea>
  implements IdeaRepository {

    @Inject
    private ApplicationConfig applicationConfig;

    //...

    @Override
    public List<CategoryView> getHighestRatedCategories() {
      return entityManager.createQuery("...")
        .setMaxResults(
          applicationConfig.maxNumberOfHighestRatedCategories())
        .getResultList();
    }
}
In unserem Fall haben wir in ApplicationConfig bisher nur eine Methode, wodurch der Aufwand unverhältnismäßig hoch erscheint. Ziel von Partial-Beans ist es natürlich Interfaces mit mehreren Methoden zu verwenden, bzw. mehrere Partial-Beans mit einem generischen Handler zu verbinden.

 

Bei der Definition weiterer Konfigurationsbeans profitieren wir unmittelbar von der zugrundeliegenden Idee. Listing MonitoringConfig als einfaches CDI-Beans zeigt die Klasse MonitoringConfig , welche wir in diesem Kapitel hinzugefügt haben.
@ConfigScoped
public class MonitoringConfig {
  @Inject
  @ConfigProperty(name = "methodInvocationThreshold")
  private Integer methodInvocationThreshold;

  public Integer getMethodInvocationThreshold() {
    return methodInvocationThreshold;
  }
}
Das Vorgehen ist zwar sehr direkt, allerdings können wir diese Konfigurationsklasse wesentlich vereinfachen. Listing MonitoringConfig als PartialBean zeigt die Umstellung von MonitoringConfig auf ein Partial-Bean. Da wir sowohl das Partial-Bean-Binding als auch den generischen Handler bereits umgesetzt haben, ist der Erstellungsaufwand für alle weiteren Partial-Beans bzw. die Erweiterung um zusätzliche Methoden minimal.
@TypedConfig
public interface MonitoringConfig {
  Integer methodInvocationThreshold();
}
Bei der Umstellung von MonitoringConfig haben wir jedoch einen Mechanismus verloren. Die geladenen Werte werden jetzt nicht mehr in unserem Config-Context gespeichert und müssen daher bei jedem Zugriff erneut durch DeltaSpike geladen werden. Wir könnten unser Partial-Bean selbst wieder mit der @ConfigScoped -Annotation versehen. Allerdings würden wir in diesem Fall nur die intern generierte Instanz in diesem Context ablegen. Methodenaufrufe würden weiterhin an den generischen Handler weitergeleitet werden, welcher bei jedem Zugriff den Wert erneut lädt. Aus diesem Grund müssen wir den generischen Handler selbst im Config-Context speichern und die geladenen Werte lokal cachen. Listing Konfigurationswerte in TypedConfigHandler cachen illustriert die erforderlichen Änderungen in der Klasse TypedConfigHandler .
@TypedConfig
@ConfigScoped
public class TypedConfigHandler implements InvocationHandler {
  private Map<String, Object> loadedValues =
    new ConcurrentHashMap<String, Object>();

  public Object invoke(Object proxy, Method method, Object[] args)
    throws Throwable {

      String key = method.getName();
      Object result = loadedValues.get(key);

      if (result != null) {
        return result;
      }

      String loadedValue = ConfigResolver.getPropertyValue(key);

      Class<?> configType = method.getReturnType();
      result = parseValue(loadedValue, configType);

      loadedValues.put(key, result);
      return result;
  }

  //...
}
Tipp: Falls es erforderlich werden sollte einzelne Methoden eines Partial-Beans manuell zu implementieren, kann hierfür auf abstrakte Klassen zurückgegriffen werden. Abstrakte Methoden werden hierbei weiterhin an den entsprechenden generischen Handler weitergeleitet. Explizit implementierte Methoden werden hingegen normal ausgeführt, wodurch der Handler in solchen Fällen übergangen wird.
Dieser Ansatz hat zusätzlich den Vorteil, dass wir Veränderungen zentral umsetzen können. Statt jede Konfigurationsklasse einzeln mit @ConfigScoped zu annotieren, ist dies jetzt nur für die Klasse TypedConfigHandler erforderlich. Auch andere Änderungen könnten wir zentral für alle Konfigurationen vornehmen. So könnten wir bspw., wie in Listing Umstellung auf Project-Stage abhängige Konfigurationswerte dargestellt, statt der Methode ConfigResolver#getPropertyValue die Methode ConfigResolver#getProjectStageAwarePropertyValue verwenden, um Konfigurationswerte an einen Project-Stage zu binden.
@TypedConfig
@ConfigScoped
public class TypedConfigHandler implements InvocationHandler {
  //...

  public Object invoke(Object proxy, Method method, Object[] args)
    throws Throwable {

    //...
    String loadedValue =
      ConfigResolver.getProjectStageAwarePropertyValue(key);

    //...
    return result;
  }

  //...
}
Diese Änderung erlaubt es Konfigurationswerte für bestimmte Project-Stages zu überschreiben. Für den Key maxNumberOfHighestRatedCategories lautet unser Konfigurationseintrag bspw. maxNumberOfHighestRatedCategories=10 . Dieser Eintrag ist weiterhin gültig, außer es gibt für den aktuellen Project-Stage einen expliziten Eintrag. Würden wir den Project-Stage Development aktivieren, dann könnten wir den Eintrag maxNumberOfHighestRatedCategories=10 bspw. mit maxNumberOfHighestRatedCategories.Development=100 übersteuern. So könnten wir während der Entwicklung bspw. Versuche durchführen, wie sich die Seite bei der Darstellung einer hohen Anzahl an Kategorien verhält ohne Gefahr zu laufen eine produktive Konfiguration irrtümlich zu verändern.

 

Manche Konfigurationen erfordern nicht unbedingt eine regelmäßige Aktualisierung. In IdeaFork haben wir in diesem Kapitel bspw. einen CDI-Qualifier namens ApplicationName erstellt, welcher zusätzlich mit @ConfigProperty annotiert war, damit der konfigurierte Applikationsname typsicher injizierbar wird und gleichzeitig die dazugehörige Producer-Logik den konfigurierten Wert generisch auf Basis von @ConfigProperty laden kann. Die Umstellung der Konfigurationsklassen in IdeaFork auf Partial-Beans berücksichtigt bisher allerdings keine CDI-Qualifier. Ersetzen wir den bisher verwendeten ConfigProducer , so würden wir ein Partial-Bean namens ApplicationConfig erhalten, wie es in Listing Partial-Bean ohne Producer zu sehen ist.
@TypedConfig
public interface ApplicationConfig {
  Integer maxNumberOfHighestRatedCategories();

  String name();

  ApplicationVersion version();

  ExternalFormat.TargetFormat defaultExternalFormat();
}
Entsprechend könnten wir bspw. den Applikationsnamen nicht mehr als String in Kombination mit dem @ApplicationName -Qualifier injizieren. Damit wir diese Möglichkeit weiterhin nutzen können, ist eine kleine Erweiterung des Partial-Beans erforderlich. Wie in Listing Partial-Bean mit Producer dargestellt, können wir auch Methoden in einem Partial-Bean mit @Produces und optional mit einer Qualifier-Annotation annotieren. Producer-Methoden in Partial-Beans agieren wie herkömmliche CDI-Producer mit dem einzigen Unterschied, dass auch hier der Partial-Bean-Handler die Ausführung der Methoden übernimmt. Ein positiver Zusatzaspekt ist, dass wir Qualifier, wie in unserem Fall die @ApplicationName -Annotation, nicht mehr mit @ConfigProperty markieren müssen.
@TypedConfig
public interface ApplicationConfig {
  Integer maxNumberOfHighestRatedCategories();

  @Produces
  @ApplicationName
  String name();

  @Produces
  ApplicationVersion version();

  @Produces
  ExternalFormat.TargetFormat defaultExternalFormat();
}
Da wir bei keiner Producer-Methode in ApplicationConfig eine Scope-Annotation verwenden, ist das Ergebnis hier immer ein dependent-scoped Bean. Wie auch bisher können so erzeugte Konfigurationswerte nur dann aktualisiert werden, wenn der entsprechende Injection-Point neu befüllt wird. Natürlich könnten wir auch ApplicationConfig injizieren und direkt auf sämtliche Methoden zugreifen. Bei Informationen wie dem Applikationsnamen sind wir jedoch nicht darauf angewiesen durch den direkten Aufruf von ApplicationConfig#name immer den aktuell gecachten Wert zu verwenden und daher können wir solche Informationen auch direkt ohne den Umweg über ApplicationConfig in anderen CDI-Beans verwenden.

 

In ApplicationConfig haben wir auch das Ergebnis von defaultExternalFormat als dependent-scoped Bean definiert. Hierfür gibt es allerdings einen technischen Grund, denn für Enum-Werte kann kein Proxy erzeugt werden, wodurch wir keine CDI-Normal-Scopes verwenden können. In unserem Fall möchten wir CurrentObjectConverterProducer vereinfachen, da bisher der konfigurierte Wert mit Hilfe von @ConfigProperty injiziert und der String manuell ausgewertet wurde. Listing Aktivierung von Implementierungen nach Konfigurationsänderungen zeigt die geänderte Implementierung von CurrentObjectConverterProducer . Durch den Producer für ExternalFormat.TargetFormat können wir den aktuellen Wert direkt injizieren. Die vorherige Version von CurrentObjectConverterProducer hat den selektierten ObjectConverter als dependent-scoped Bean für den @Default -Qualifier zur Verfügung gestellt. Würden wir dies weiterhin machen, dann würde sich ein Refresh der Konfiguration nicht konsistent in der Applikation verbreiten. Injizieren wir bspw. den @Default - ObjectConverter in einem application-scoped Bean, dann würde dieses Bean den dependent-scoped ObjectConverter bis zum Neustart der Applikation verwenden. Eine Injizierung in einem request-scoped Bean würde hingegen dazu führen, dass wir je Request eine neue Referenz auf den aktuellen ObjectConverter erhalten. Ändert sich der konfigurierte Wert für den Key defaultExternalFormat während der Laufzeit, dann würde das request-scoped Bean diesen neuen Wert erhalten, sobald der Cache in TypedConfigHandler aktualisiert wird. Das application-scoped Bean würde hingegen weiterhin mit dem ursprünglichen ObjectConverter arbeiten. Um solche Inkonsistenzen zur Laufzeit zu vermeiden, können wir die Producer-Methode in Listing Aktivierung von Implementierungen nach Konfigurationsänderungen zusätzlich um die Annotation @ConfigScoped erweitern. Durch diese Änderung wird der aktuelle ObjectConverter für alle Injection-Points aktualisiert, sobald der Config-Context zurückgesetzt wird. Das heißt gleichzeitig, dass alle Beans immer den aktuellen ObjectConverter verwenden unabhängig von ihrem Scope.
@ApplicationScoped
public class CurrentObjectConverterProducer {
  @Produces
  @Default
  @ConfigScoped
  protected ObjectConverter defaultConverter(
    @ExternalFormat(XML) ObjectConverter objectConverterXml,
    @ExternalFormat(JSON) ObjectConverter objectConverterJson,
    ExternalFormat.TargetFormat defaultFormat) {
      switch (defaultFormat) {
        case JSON:
          return objectConverterJson;
        default:
          return objectConverterXml;
      }
    }
}
Das Ergebnis unserer Umstellung ist eine einfache, einheitliche und aktualisierbare Konfiguration, die darüber hinaus typsicher ist. Für jeden neuen Konfigurationseintrag muss im einfachsten Fall nur eine Methode in einem Interface hinzugefügt werden. Ändert sich der Key des Konfigurationseintrags, dann muss nur diese eine Methode entsprechend umbenannt werden. Alle modernen Java-IDEs stellen bei einem solchen Refactoring die dazugehörigen Methodenaufrufe automatisch um. Wird dies nicht korrekt gemacht, dann würde spätestens der Java-Compiler einen Fehler melden. Die gesamte Logik zum Laden, Parsen und Cachen der Werte kann zentral in einem Handler durchgeführt werden. Änderungen können somit ebenfalls zentral durchgeführt werden.

 

Dennoch haben wir eine wichtige Fehlerquelle bei unserer typsicheren Konfiguration noch nicht beseitigt. Wir haben zwar sichergestellt, dass wir typsicher auf die konfigurierten Werte zugreifen, aber wir wissen nicht, ob jede Interface-Methode wirklich zu einem konfigurierten Wert führt. So könnte sich bspw. der Key eines Eintrags ändern und unsere Applikation würde weiterhin normal starten, selbst wenn die dazugehörige Methode nicht umbenannt wurde. Die Auswirkung wird folglich erst zur Laufzeit beim Zugriff auf die Methode sichtbar.

 

Um dies zu vermeiden können wir das Interface org.apache.deltaspike.core.spi.config.ConfigValidator implementieren und in der Datei META-INF/services/org.apache.deltaspike.core.spi.config.ConfigValidator aktivieren. Damit wir die Interfaces finden, die mit unserer selbst erstellte Annotation @TypedConfig annotiert sind, implementiert unser Validator zusätzlich javax.enterprise.inject.spi.Extension und muss ebenfalls in der Datei META-INF/services/javax.enterprise.inject.spi.Extension hinzugefügt werden. In Listing Extension zur Validierung typsicherer Konfiguration wird diese Implementierung TypedConfigValidationExtension genannt. Die Methode #findTypedConfigClasses überprüft, ob es sich bei dem gefundenen Bean um eine Implementierung von InvocationHandler handelt, welche zusätzlich mit @TypedConfig annotiert ist. In diesem Fall wird die gefundene Klasse einer Liste hinzugefügt, welche in der Methode #processValidation für die Validierung verwendet wird. Bei der Validierung wird jeder Methodenname als Key für den Methodenaufruf getPropertyValue genutzt. Kann für einen Key kein konfigurierter Wert gefunden werden, dann wird eine passende Fehlermeldung erzeugt. Hätten wir optionale Konfigurationen, dann müssten wir zusätzlich eine weitere Annotation für Methoden einführen. Diese könnte bspw. @OptionalKey heißen und Methoden mit einer solchen Annotation würden einfach bei der Validierung übersprungen werden.
public class TypedConfigValidationExtension
  implements ConfigValidator, Extension {

  private static List<Class> foundConfigClasses =
    new CopyOnWriteArrayList<Class>();

  public void findTypedConfigClasses(@Observes ProcessAnnotatedType pat) {
    Class<?> beanClass = pat.getAnnotatedType().getJavaClass();
    TypedConfig typedConfig = beanClass.getAnnotation(TypedConfig.class);

    if (typedConfig != null &&
        !InvocationHandler.class.isAssignableFrom(beanClass)) {
      foundConfigClasses.add(beanClass);
    }
  }

  @Override
  public Set<String> processValidation() {
    Set<String> violations = new HashSet<String>();
    for (Class configClass : foundConfigClasses) {
      validateConfigKeys(configClass.getMethods(), violations);
    }

    foundConfigClasses.clear();
    return violations;
  }

  private void validateConfigKeys(
    Method[] methods, Set<String> violations) {

      for (Method method : methods) {
        String key = method.getName();
        String configuredValue = ConfigResolver.getPropertyValue(key);

        if (configuredValue == null) {
          violations.add("missing config-key: " + key);
        }
      }
  }
}
Tipp: Manche Server sind nicht spezifikationskonform, wodurch wir in der Klasse TypedConfigValidationExtension eine statische Variable verwenden müssen. In spezifikationskonformen Servern könnten wir auch eine Instanzvariable verwenden, wodurch es einfacher wird solche Extensions in geteilten Modulen für mehrere Module zu verwenden. In diesem Fall müssten wir in der Methode #processValidation einen Bean-Lookup auf TypedConfigValidationExtension verwenden, damit wir die Extension -Instanz mit der befüllten Liste bekommen.

5.9 Flexibilität weiter erhöhen

In vielen Projekten wird zusätzlich die Möglichkeit geschaffen konfigurierte Werte aus einer Datenbank zu laden, damit notfalls zur Laufzeit eine Konfiguration geändert werden kann, ohne die Applikation neu zu deployen. Dieses Konzept ist im Normalfall einfach und schnell umsetzbar. In unserem Fall wollen wir diese Implementierung möglichst minimalistisch halten und zusätzlich mit dem zuvor vorgestellten Konfigurationsmechanismus von DeltaSpike integrieren.

 

Im ersten Schritt erstellen wir eine einfache JPA-Entität namens ConfigEntry , welche in Listing JPA-Entität für dynamische Konfigurationen zu sehen ist. Diese Entität könnten wir wie gewohnt mit Hilfe einer Repository-Implementierung verwenden. Mit GenericJpaRepository haben wir bereits eine Basisimplementierung im Core von IdeaFork mit welcher wir nur eine konkrete Implementierung auf unseren neuen Entity-Typ typisieren müssten. In DeltaSpike wird ein ähnlicher Ansatz durch das Data-Modul zur Verfügung gestellt, wodurch wir auf eine eigene Basisimplementierung verzichten und von zusätzlichen Funktionalitäten profitieren können.
@Entity
public class ConfigEntry extends BaseEntity {
  @Column(unique = true, nullable = false)
  private String entryKey;

  @Column
  private String value;

  //+ getter and setter ...
}
Listing Entity-Repository auf Basis von DeltaSpike-Data zeigt ein Partial-Bean, welches durch einen generischen Handler im Data-Modul von DeltaSpike zum Leben erweckt wird. Die Annotation @org.apache.deltaspike.data.api.Repository markiert dieses Interface als Partial-Bean, wodurch der Handler von DeltaSpike Queries auf Basis der Methodennamen bzw. der optionalen Metadaten erzeugt. In unserem Fall fügen wir mit @Query(singleResult = OPTIONAL) Metadaten hinzu, die ein optionales Ergebnis ermöglichen. Außerdem leitet ConfigRepository von dem Interface org.apache.deltaspike.data.api.EntityRepository ab, wodurch Methoden wie bspw. #save , #remove , #count , #findAll und viele mehr verwendet werden können. Bei der Typisierung geben wir neben dem Entity-Typ auch den Typ des Primärschlüssels an. In unserem Fall ist der Entity-Typ die Klasse ConfigEntry und der Primärschlüssel ist in BaseEntity als String definiert.
@org.apache.deltaspike.jpa.api.transaction.Transactional
@org.apache.deltaspike.data.api.Repository
public interface ConfigRepository
  extends EntityRepository<ConfigEntry, String> {

    @Query(singleResult = OPTIONAL)
    ConfigEntry findByEntryKey(String key);
}
Unser Repository könnten wir ohne weitere Annotation bspw. in EJBs oder transaktionale Services wie üblich mit @Inject injizieren. Anschließend können wir die vordefinierten oder selbst definierten Methoden aufrufen.

 

Soll das Repository selbst transaktional sein, dann muss dieses zusätzlich mit @Transactional markiert werden. Partial-Beans haben allerdings kleine Einschränkungen, da bspw. keine CDI-Decoratoren verwendbar sind. Werden Decoratoren benötigt, müssten wir bei GenericJpaRepository von IdeaFork -Core bleiben. Aus diesem Grund stellen wir die restlichen Repositories von IdeaFork -Core nicht um.

 

In IdeaFork möchten wir ConfigRepository mit einer zusätzlichen ConfigSource einbinden. Die Klasse DataBaseAwareConfigSource aus Listing Config-Source zum Laden von Werten aus einer Datenbank implementiert diesmal direkt das Interface org.apache.deltaspike.core.spi.config.ConfigSource . Als Ordinal-Wert wählen wir einen hohen Wert der dafür sorgt, dass diese Config-Source als erste in der Kette befragt wird. Die Methode #getProperties muss nur sinnvoll implementiert werden, wenn die Methode #isScannable den Wert "true" zurückliefert. Dies ist nur erforderlich, wenn wir in unserer Applikation die Methode ConfigResolver#getAllProperties benötigen. Die Methode #getConfigName wird primär für Logeinträge benötigt, damit bspw. eine mögliche Fehlersuche einfacher wird. Der Hauptteil von DataBaseAwareConfigSource ist in der Methode #getPropertyValue zu finden. Mit BeanManagerProvider#isActive können wir überprüfen, ob BeanProvider bereits verwendet werden kann. Diesen benötigen wir nämlich für einen dynamischen Lookup von ConfigRepository . Zusätzlich können wir noch Keys die mit "deltaspike." beginnen herausfiltern, da wir die Konfiguration von DeltaSpike selbst nicht aus der Datenbank laden wollen. Sobald unser Partial-Bean namens ConfigRepository gefunden wird, können wir über die Methode #findByEntryKey versuchen den Konfigurationseintrag in der Datenbank für den gesuchten Key zu laden. Wird ein entsprechender Eintrag gefunden, dann verwenden wir das Ergebnis der Methode #getValue .
public class DataBaseAwareConfigSource implements ConfigSource {
  private final static int ordinal = 2000;

  @Override
  public int getOrdinal() {
    return ordinal;
  }

  @Override
  public String getPropertyValue(String key) {
    if (!BeanManagerProvider.isActive() ||
        key.startsWith("deltaspike.")) {

          return null;
    }

    ConfigRepository configRepository =
      BeanProvider.getContextualReference(ConfigRepository.class, true);

    if (configRepository != null) {
      ConfigEntry configEntry = configRepository.findByEntryKey(key);

      if (configEntry != null) {
        return configEntry.getValue();
      }
    }
    return null;
  }

  @Override
  public String getConfigName() {
    return "config-db";
  }

  @Override
  public boolean isScannable() {
    return false;
  }

  @Override
  public Map<String, String> getProperties() {
    return Collections.emptyMap();
  }
}
Implementierungen von org.apache.deltaspike.core.spi.config.ConfigSource werden von DeltaSpike nicht als CDI-Beans verwendet, da dieser Mechanismus ursprünglich primär dazu gedacht war Teile von DeltaSpike selbst zu konfigurieren, die schon während des Containerstartes fixiert sein müssen. Folglich handelt es sich um ein klassisches Java-SPI und wir müssen unsere Klasse voll qualifiziert in der Datei META-INF/services/org.apache.deltaspike.core.spi.config.ConfigSource aktivieren.

 

Mit diesem letzten Schritt haben wir unsere typsichere Konfiguration um eine zusätzliche Konfigurationsquelle erweitert, ohne eine weitere Anpassung in IdeaFork vorzunehmen. Default-Werte können wir weiterhin in den statischen Konfigurationsquellen hinterlegen. Wird eine Änderung zur Laufzeit benötigt, dann können wir den neuen Wert in der Datenbank hinterlegen. Sobald die gecachte Konfiguration manuell oder automatisch aktualisiert wird, werden die neuen Werte aus der Datenbank durch das höhere Ordinal unserer Konfigurationsquelle bevorzugt und somit aktiv.

 

In IdeaFork können wir dies beim Start der Applikation simulieren, wenn wir den Project-Stage Development verwenden. In Java EE gibt es viele Möglichkeiten Initialisierungs-Code umzusetzen. So kann bspw. die @Startup -Annotation für EJBs, das ServletContainerInitializer -Interface für Servlets oder das JSF-Event PostConstructApplicationEvent verwendet werden. Seit CDI 1.1 und somit Java EE 7 können auch Observer-Methoden für die Überwachung von Standard-Scopes verwendet werden. Eine Observer-Methode, welche @Observes @Initialized(ApplicationScoped.class) verwendet, wird bspw. aufgerufen sobald der Application-Context initialisiert wurde. Alle Varianten haben gemein, dass nur ein Teil des Servers gestartet ist. Keine dieser Varianten garantiert jedoch die Umsetzung einer portablen Initialisierungslogik, welche nach dem vollständigen Serverstart ausgeführt wird. Für Java EE Applikationen mit JSF-Seiten, wie es IdeaFork ist, können wir ein JSF-Add-on für DeltaSpike verwenden. Dieses Add-on führt die Initialisierungslogik beim ersten JSF-Request aus, wodurch garantiert ist, dass der gesamte Java EE Container vollständig initialisiert ist. Dieses Add-on veranschaulicht zusätzlich den Konfigurationsmechanismus von DeltaSpike. Das Add-on definiert den Konfigurationskey first-faces-request_event-class , um die Klasse des Startup-Events festzulegen. Standardmäßig wird hierfür die Klasse FirstFacesRequestEvent festgelegt. Dieser Konfigurationseintrag ist in der Datei META-INF/apache-deltaspike.properties des Add-ons abgelegt. In der gleichen Datei wird mit deltaspike_ordinal=1 ein sehr niedriger Ordinal-Wert definiert. Da der Standardwert höher ist, müssten wir in META-INF/apache-deltaspike.properties von IdeaFork keinen Wert für deltaspike_ordinal vergeben, damit die Default-Konfiguration übersteuert wird. Bei der Konfiguration einer gobalen Transaktionsstrategie haben wir allerdings bereits den Wert 1000 gewählt, welchen wir natürlich ebenfalls beibehalten können. In IdeaFork wollen wir die Klasse IdeaForkStartedEvent verwenden und fügen daher einen entsprechenden Eintrag in der Datei META-INF/apache-deltaspike.properties von IdeaFork hinzu. Das Add-on für DeltaSpike verwendet ebenfalls die zuvor vorgestellte Methode ConfigResolver#getProjectStageAwarePropertyValue , wodurch wir bspw. für den Project-Stage Development ein eigenes Event festlegen können. Listing Stageabhängiges Startup-Event veranschaulicht beide Konfigurationseinträge. Bei beiden Klassen handelt es sich um simple (Marker-)Klassen ohne zusätzliche Logik. Im Falle von Project-Stage Development wird zuerst der Wert für first-faces-request_event-class.Development gesucht. Erst wenn dieser nicht vorhanden ist, dann wird der Wert für first-faces-request_event-class geladen.
first-faces-request_event-class=
  at.irian.cdiatwork.ideafork.core.api.startup.IdeaForkStartedEvent
first-faces-request_event-class.Development=
  at.irian.cdiatwork.ideafork.ee.infrastructure.DevStartupEvent
Listing Stageabhängiges Startup-Event überwachen zeigt wie wir ein solches Startup-Event verwenden können. Im konkreten Fall ändern wir bei Project-Stage Development einen konfigurierten Wert ab, indem wir den gewünschten Wert mit Hilfe der Entität ConfigEntry in der Datenbank speichern. Bei allen nachfolgenden Zugriffen auf maxNumberOfHighestRatedCategories , wird der in der Datenbank konfigurierte Wert verwendet.
public class DataImporter {
  @Inject
  private ConfigRepository configRepository;

  protected void init(@Observes DevStartupEvent devStartupEvent) {
    configRepository.save(
      new ConfigEntry("maxNumberOfHighestRatedCategories", "2"));
  }
}
Alternativ zu DevStartupEvent könnten wir einen Observer für das Event IdeaForkStartedEvent in einem Bean definieren, welches mit @Exclude(exceptIfProjectStage = ProjectStage.Development.class) annotiert ist.

 

In unserer Applikation können wir die Klasse DevStartupEvent zusätzlich von IdeaForkStartedEvent ableiten, wodurch beim Feuern des Events DevStartupEvent auch Observer für das Event IdeaForkStartedEvent aufgerufen werden. Der Observer in Listing Observer für ein portables Startup-Event wird somit sowohl in Project-Stage Development als auch in Project-Stage Production aufgerufen. In unserem Fall verwenden wir diesen Observer, um DataBaseAwareConfigSource dynamisch hinzuzufügen.
@ApplicationScoped
public class IdeaForkCoreStartupObserver {
    protected void onStartup(
      @Observes IdeaForkStartedEvent ideaForkStartedEvent,
      DataBaseAwareConfigSource configSource) {

        ConfigResolver.addConfigSources(
          Arrays.<ConfigSource>asList(configSource));
    }
}
Durch die dynamische Registrierung ist es nicht mehr erforderlich die Config-Source-Implementierung in der Datei META-INF/services/org.apache.deltaspike.core.spi.config.ConfigSource zu konfigurieren. Ein weiterer Vorteil ist die einfachere Implementierung von DataBaseAwareConfigSource , welche in Listing Änderung von DataBaseAwareConfigSource zu sehen ist. Statt der manuellen Verwendung von BeanManagerProvider und BeanProvider können wir ConfigRepository wie gewohnt via @Inject injizieren, weil DataBaseAwareConfigSource jetzt vom CDI-Container verwaltet wird.
@ApplicationScoped
public class DataBaseAwareConfigSource implements ConfigSource {
  //...

  @Inject
  private ConfigRepository configRepository;

  @Override
  public String getPropertyValue(String key) {
    if (key.startsWith("deltaspike.")) {
      return null;
    }

    ConfigEntry configEntry = configRepository.findByEntryKey(key);

    if (configEntry != null) {
      return configEntry.getValue();
    }
    return null;
  }

  //...
}

5.10 Besser früher als Später

Wie wir in diesem Kapitel bereits gesehen haben, erweitert DeltaSpike die Java EE Plattform mit vielfältigen und innovativen Konzepten. Ein weiterer Aspekt von DeltaSpike ist die frühzeitige Bereitstellung von neuen Java EE Konzepten für ältere Versionen von Java EE.

 

Beispiele hierfür sind das Servlet- und das Bean-Validation Modul. Das Servlet-Modul ermöglicht bspw. eine portable Injizierung der aktuellen HttpServletResponse -Instanz. In einem Java EE6 Server wird dies bereits mit @Context unterstützt. Erst ab Java EE7 kann hierfür auch @Inject verwendet werden. Das Servlet-Modul von DeltaSpike erlaubt die Injizierung via @Inject in Verbindung mit dem Qualifier @DeltaSpike . Durch den Qualifier ist der Injection-Point nicht nur mit Java EE6 und in einem Servlet-Container mit CDI verwendbar, sondern auch ohne Änderung in einem Java EE7 Server und jeder nachfolgenden Version. Die Funktionalität steht folglich schon vor EE7 zur Verfügung und ist darüber hinaus völlig portabel. Listing Injizierung via @DeltaSpike Qualifier zeigt einen Ausschnitt von IdeaExporter , der entsprechend angepasst ist. Sobald das Servlet-Modul von DeltaSpike hinzugefügt wird, kann der Injection-Point in IdeaExporter befüllt werden.
@Path("/idea/")
@Produces(MediaType.APPLICATION_JSON)
public class IdeaExporter {
    @Inject
    @DeltaSpike
    private HttpServletResponse response;

  //...
}
Neben der Injizierung von (Http)ServletResponse unterstützt das Servlet-Modul noch die Injizierung der aktuellen ServletContext -, (Http)ServletRequest -, HttpSession - und Principal -Instanz.

 

Einen ähnlichen Vorteil bietet das Bean-Validation Modul von DeltaSpike. Im vorherigen Kapitel haben wir BeanAwareConstraintValidatorFactory implementiert, wodurch wir CDI-basierte Injizierung in Constraint-Validatoren ermöglicht haben. Genau diese Funktionalität stellt auch DeltaSpike bereit. Hierfür müssen wir das Bean-Validation-Modul hinzufügen und den Konfigurationseintrag in der Datei validation.xml ändern. Listing Aktivierung von CDIAwareConstraintValidatorFactory illustriert den neuen Inhalt dieser Konfigurationsdatei.
<validation-config>
  <constraint-validator-factory>
    org.apache.deltaspike.beanvalidation.impl
      .CDIAwareConstraintValidatorFactory
  </constraint-validator-factory>
</validation-config>
Im Gegensatz zu der ursprünglichen Implementierung namens BeanAwareConstraintValidatorFactory , kann CDIAwareConstraintValidatorFactory von DeltaSpike auch mit Java EE7 verwendet werden. EE7 stellt diese Funktionalität zwar bereits automatisch zur Verfügung, aber durch die zusätzliche Kompatibilität mit EE7 ist IdeaFork ohne Änderung auch mit EE7-Servern kompatibel. Mit EE7 könnten wir diese Konfiguration und das dazugehörige Modul zwar komplett aus IdeaFork entfernen, aber in diesem Fall wäre IdeaFork nicht mehr mit EE6 kompatibel.

5.11 Sichere Wege

Wir schließen dieses Kapitel mit einem Page-Bean-Test ab. In unserem Fall möchten wir den Promotion-Wizard typsicher testen. Abgesehen von der Page-Bean Logik soll auch das persistierte Ergebnis überprüft werden. Im Gegensatz zu Frameworks wie bspw. JBoss Arquillian erstellen wir keine Micro-Test-Deployments, welche in einem vollständigen EE-Server deployed und getestet werden können. Bisher haben wir CdiTestRunner aus dem Test-Control-Modul verwendet. Dieses Modul startet mit Hilfe vom CDI-Control-Modul von DeltaSpike den jeweils gewünschten CDI-Container, aber keinen kompletten EE-Server. Für Page-Bean-Tests benötigen wir allerdings auch einen gestarteten JSF-Container. Um andere Container wie bspw. einen gemockten JSF-Container zu integrieren, kann das org.apache.deltaspike.testcontrol.spi.ExternalContainer -SPI verwendet werden. Das Test-Control-Modul von DeltaSpike enthält verschiedene Adapter für MyFaces-Test. So können wir bspw. org.apache.deltaspike.testcontrol.impl.jsf.MyFacesContainerAdapter in der Datei META-INF/services/org.apache.deltaspike.testcontrol.spi.ExternalContainer aktivieren. Durch diese Konfiguration und die entsprechenden Test-Dependencies für MyFaces-Test wird nicht nur ein gemockter JSF-Container automatisch gestartet, sondern es werden auch wie für CDI der Request- und Session-Scope gestartet.

 

In Listing User Registrierung und Login via Page-Bean-Test registrieren wir über das RegistrationViewCtrl -Bean von IdeaFork einen neuen User . Unsere Page-Beans sind mit unserer eigenen Stereotyp Annotation @ViewController definiert. Dieser Stereotyp gibt den View-Access Scope von DeltaSpike als Default Scope vor. Da dieser Scope auf dem Window-Scope aufbaut, müssen wir den dazugehörigen Window-Context vor jeder Test-Methode aktivieren, sofern im Test Methoden eines Page-Beans aufgerufen werden. In unserem Test ist dies im @Before -Callback namens #initTestWindow mit dem Aufruf der Methode #activateWindow umgesetzt. Folglich können wir in unserem Test bspw. RegistrationViewCtrl#getNewUser aufrufen. Als Window-ID können wir einen beliebigen String verwenden. Bei Page-Bean-Tests wird durch diesen Aufruf vor jeder Test-Methode ein simuliertes Browser-Fenster mit der ID "testWindow" erzeugt. Wie bisher startet CdiTestRunner natürlich weiterhin für jede Test-Methode den Request- und Session-Scope erneut. Im Hintergrund speichert der Window-Context alle Daten unter der angegebenen Window-ID in der Session ab. Diese beiden Aspekte gewährleisten, dass jede Test-Methode von einem neuen Window-Context ausgehen kann.

 

Für die Tests der Promotion-Wizard-Beans benötigen wir einen registrierten und eingeloggten User . Dies könnten wir manuell mit Hilfe der zuständigen Services und Beans machen oder ebenfalls mit Page-Beans dieser Seiten. Wir entscheiden uns für die zweite Variante und beginnen in unserer Test-Methode namens #flowFromRegistrationToIdeaPromotion mit dem Aufruf der Setter-Methoden, welche normalerweise automatisch durch JSF auf Basis der Value-Bindings in der Seite registration.xhtml befüllt werden. Anschließend rufen wir die Action-Methode #register auf und überprüfen das Navigationsergebnis. Durch die Verwendung der typsicheren View-Config als Navigationsergebnis können wir auch in Tests von der Typsicherheit und den damit verbundenen Vorteile profitieren. Wir beenden die Überprüfung dieses ersten Teiles mit dem Aufruf der JSF-API, um zu überprüfen ob nicht nur das Navigationsergebnis den Erwartungen entspricht, sondern auch eine Nachricht hinzugefügt wurde. Hierin besteht auch einer der Hauptunterschiede von Page-Bean-Tests zu vollständigen UI-Tests. Der Vorteil ist die einfachere Erstellung der Tests. Allerdings müssen zumindest ein paar der Abläufe, die normalerweise automatisch ausgeführt werden, manuell nachgestellt werden. Manche Ergebnisse können darüber hinaus nicht im gerenderten Response überprüft werden, sondern über die entsprechenden JSF-APIs.
@RunWith(CdiTestRunner.class)
public class IdeaForkBaseFlowTest {
  @Inject
  private WindowContext windowContext;

  @Inject
  private RegistrationViewCtrl registrationViewCtrl;

  @Before
  public void initTestWindow() {
    windowContext.activateWindow("testWindow");
  }

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

    Assert.assertEquals(Pages.User.Login.class, navigationResult);
    Assert.assertFalse(
      FacesContext.getCurrentInstance().getMessageList().isEmpty());

    //...
  }
}
Wenn wir mit einer Test-Methode einen Use-Case testen wollen, der sich über mehrere Seiten erstreckt, ist es erforderlich die Test-Methode in mehrere logische Requests zu unterteilen. Listing Restart des Request-Scopes in einer Test-Methode veranschaulicht, wie wir mit Hilfe der CDI-Control API den Request-Scope restarten können. Nachdem der Request-Context gestoppt wird, kann dieser unmittelbar danach gleich wieder gestartet werden. Normalerweise sorgt das JSF-Modul von DeltaSpike automatisch dafür, dass die Window-ID am Anfang jedes Requests wiederhergestellt wird. Auch diesen Aspekt müssen wir manuell nachbilden indem wir in der Methode #newRequest die zuvor vorgestellte Methode #initTestWindow aufrufen.
@RunWith(CdiTestRunner.class)
public class IdeaForkBaseFlowTest {
  //...
  @Inject
  private ContextControl contextControl;

  @Test
  public void flowFromRegistrationToIdeaPromotion() {
    //...

    newRequest();

    //...
  }

  private void newRequest() {
    contextControl.stopContext(RequestScoped.class);
    contextControl.startContext(RequestScoped.class);
    initTestWindow();
  }
}
Nach der erfolgreichen Registrierung eines Test-Users starten wir einen neuen logischen Request, um den eben angelegten User über das LoginViewCtrl -Page-Bean einzuloggen. Dabei gehen wir auf ähnliche Art und Weise wie bei der Registrierung vor. Nachdem wir die entsprechenden Setter-Methoden und schließlich die Action-Methode aufgerufen haben, können wir das Navigationsergebnis überprüfen. Auch hier können wir anschließend über die JSF-API überprüfen, ob eine Nachricht hinzugefügt wurde. Um zu überprüfen ob es sich nicht um die Nachricht des vorherigen Requests handelt, beginnen wir in Listing Überprüfung von Messages in einem logischen Request mit der Überprüfung auf eine leere Message-Liste.
@RunWith(CdiTestRunner.class)
public class IdeaForkBaseFlowTest {
  //...

  @Inject
  private LoginViewCtrl loginViewCtrl;

  @Test
  public void flowFromRegistrationToIdeaPromotion() {
    //...

    Assert.assertTrue(
      FacesContext.getCurrentInstance().getMessageList().isEmpty());

    loginViewCtrl.setEmail("os890@test.org");
    loginViewCtrl.setPassword("test");
    navigationResult = loginViewCtrl.login();
    Assert.assertEquals(Pages.Idea.Overview.class, navigationResult);

    Assert.assertFalse(
      FacesContext.getCurrentInstance().getMessageList().isEmpty());

    //...
  }

  //...
}
Im vorherigen Beispiel haben wir allerdings einen Aspekt nicht beachtet. Zwischen der Ausführung der Action-Methode und dem Rendern der hinzugefügten Message wird in der realen Applikation ein Redirect durchgeführt. Dies könnten wir durch einen erneuten Aufruf der Methode #newRequest simulieren. Hätten wir dies vor der Überprüfung der Nachrichten gemacht, dann würde diese fehlschlagen. Der Grund hierfür liegt darin, dass DeltaSpike JSF-Messages nur über einen echten Redirect rettet. Wenn es nicht um die Behandlung von Nachrichten geht, können wir die Methode #newRequest dennoch zur Simulierung eines Redirects verwenden. In Listing Testen mit View-Controller Methoden können wir bspw. nach dem Aufruf der Action-Methode #save durch den Aufruf der Methode #newRequest einen neuen logischen Request starten bevor manuell die Callback-Methode #onPreRenderView aufgerufen und das Ergebnis der Methode #getSelectableIdeaList überprüft wird.
@RunWith(CdiTestRunner.class)
public class IdeaForkBaseFlowTest {
  //...

  @Inject
  private IdeaCreateViewCtrl ideaCreateViewCtrl;

  @Inject
  private NavigationController navigationController;

  @Inject
  private PromotionWizardCtrl promotionWizardCtrl;

  @Test
  public void flowFromRegistrationToIdeaPromotion() {
    //...

    newRequest();

    final String topic = "Test Page-Beans";
    final String category = "test";

    ideaCreateViewCtrl.setTopic(topic);
    ideaCreateViewCtrl.setCategory(category);
    navigationResult = ideaCreateViewCtrl.save();
    Assert.assertEquals(Pages.Idea.Overview.class, navigationResult);

    newRequest();

    navigationResult = navigationController.toIdeaPromotionWizard();
    Assert.assertEquals(
      Pages.PromotionWizard.Step1.class, navigationResult);

    newRequest(); //simulates a redirect

    promotionWizardCtrl.onPreRenderView();

    List<SelectableEntity<Idea>> selectableIdeas =
      promotionWizardCtrl.getSelectableIdeaList();
    Assert.assertNotNull(selectableIdeas);
    Assert.assertEquals(1, selectableIdeas.size());

    //...
  }

  //...
}
In IdeaFork verwenden wir eine Tabelle für die Auswahl von Ideen, die promotet werden können. Auch hier müssen wir in unserem Page-Bean-Test eine Idee auswählen und manuell der Action-Methode namens #select übergeben. Im vorherigen Listing haben wir der lokalen Variable selectableIdeas das Ergebnis der Page-Bean-Methode #getSelectableIdeaList bereits zugewiesen. In Listing Auswahl eines Eintrags wählen wir einen Eintrag aus dieser Liste und übergeben diesen der Methode promotionWizardCtrl#select . Anschließend navigieren wir wie gewohnt mit dem nachfolgendem Request auf die nächste Seite des Wizards. Mit dem Aufruf der Action-Methode #toStep2 bestätigen wir diesen Wizard-Schritt zur Auswahl einer Idee. In den verbleibenden Wizard-Schritten geben wir eine Beschreibung für den Promotion-Request und speichern diesen schließlich.
@RunWith(CdiTestRunner.class)
public class IdeaForkBaseFlowTest {
  //...

  @Test
  public void flowFromRegistrationToIdeaPromotion() {
    //...

    newRequest();

    promotionWizardCtrl.select(selectableIdeas.iterator().next());

    newRequest();

    navigationResult = promotionWizardCtrl.toStep2();
    Assert.assertEquals(
      Pages.PromotionWizard.Step2.class, navigationResult);

    newRequest(); //simulates a redirect

    promotionWizardCtrl.onPreRenderView();

    newRequest();

    promotionWizardCtrl.getPromotionRequest()
      .setDescription("promote it");
    navigationResult = promotionWizardCtrl.showConfirmation();
    Assert.assertEquals(
      Pages.PromotionWizard.FinalStep.class, navigationResult);

    newRequest(); //simulates a redirect

    promotionWizardCtrl.onPreRenderView();

    newRequest();

    promotionWizardCtrl.savePromotionRequest();

    //...
  }

  //...
}
Wie man Page-Bean-Tests im Detail umsetzt, kann auf die konkreten Gegebenheiten angepasst werden. Auch in Page-Bean-Tests können wir aus technischer Sicht wie bisher beliebige CDI-Beans verwenden. In Listing Direkter Zugriff auf Services wählen wir für den letzten Schritt eine Abkürzung über das IdeaService -Bean. Da für die Bestätigung von Promotion-Requests ein anderer User benötigt wird, müssten wir mit einem Test der nur Page-Beans verwendet diesen User erneut registrieren und einloggen. Erst dann könnten wir mit PromotionRequestListViewCtrl die Promotion-Requests anderer User überprüfen.
@RunWith(CdiTestRunner.class)
public class IdeaForkBaseFlowTest {
  //...

  @Inject
  private IdeaService ideaService;

  @Test
  public void flowFromRegistrationToIdeaPromotion() {
    //...

    newRequest();

    User testUser = new User("tester", null, null);
    List<PromotionRequest> foundPromotionRequests =
      ideaService.loadRecentIdeaPromotions(testUser, "*");
    Assert.assertNotNull(foundPromotionRequests);
    Assert.assertEquals(1, foundPromotionRequests.size());

    PromotionRequest loadedPromotionRequest =
      foundPromotionRequests.iterator().next();

    Assert.assertEquals("promote it",
      loadedPromotionRequest.getDescription());
    Assert.assertEquals(topic,
      loadedPromotionRequest.getIdeaForPromotion().getTopic());
    Assert.assertEquals(category,
      loadedPromotionRequest.getIdeaForPromotion().getCategory());

    //...
    newRequest();

    foundPromotionRequests =
      ideaService.loadRecentIdeaPromotions(testUser, "x");
    Assert.assertNotNull(foundPromotionRequests);
    Assert.assertTrue(foundPromotionRequests.isEmpty());
  }

  //...
}
Tipp: Im Git-Repository von IdeaFork sind neben den beiden Überprüfungen zu #loadRecentIdeaPromotions noch weitere Varianten enthalten, welche verschiedene Parameterwerte überprüfen.
In unserem Test haben wir einen kompletten Durchlauf von der Registrierung eines neuen Users bis hin zur Überprüfung von Promotion-Requests anderer User veranschaulicht. Dabei haben wir verschiedene Aspekte wie die Simulierung neuer Requests, Verwendung von Page-Beans bis hin zur typsicheren Überprüfung von Navigationsergebnissen in Tests kennengelernt. In der Praxis werden je Test-Methode eher kleinerer Ausschnitte der Applikation getestet. Auch für solche Fälle können bspw. Daten zuerst mit anderen CDI-Beans erzeugt werden, um mit dem so erzeugten Zustand in der Applikation einzelne Page-Beans zu testen.

 

Tipp: Tests, die von UI-Logik bis hin zur Datenzugriffsschicht komplette Use-Cases überprüfen, dauern im Normalfall etwas länger als simple Unit-Tests und daher wird im Git-Repository von IdeaFork ein eigenes Test-Profil namens DeltaSpikeTest für diese Tests verwendet.
In diesem Kapitel haben wir uns einen groben Überblick über den Funktionsumfang von Apache DeltaSpike verschafft. Außerdem haben wir mit einer eigenen typsicheren Konfiguration die verfügbaren Mechanismen erweitert. Im nächsten Kapitel werden wir mit Hilfe von DeltaSpike andere Container wie bspw. Spring und Akka mit CDI integrieren.