4 Portable CDI-Erweiterungen
Im Dezember 2009 war es soweit und CDI stand in einer ersten Version als Teil von Java EE6 zur Verfügung. Das vorherige Kapitel hat gezeigt, dass CDI sehr früh mit anderen Java EE6 Spezifikationen wie bspw. JSF, EJB, u.v.m. integriert wurde. Allerdings gibt es bei der standardmäßigen Integration teilweise Verbesserungspotential. Dieses wurde in einigen Fällen schnell erkannt und bei der nächsten Gelegenheit, dem Release von Java EE7, nachgereicht. Abgesehen davon können bestimmte Funktionalitäten wie bspw. die Integration anderer Frameworks nur schwer oder gar nicht durch Spezifikationen abgedeckt werden. Damit diese Einschränkungen bei der täglichen Arbeit nicht zu einem limitierenden Faktor werden, wurde das SPI (Service Provider Interface) von CDI sehr flexibel gehalten. Dadurch wird es möglich sogenannte portable CDI Erweiterungen zu implementieren, um das Standard-API von CDI zu erweitern und mögliche Lücken zu schließen. Mittlerweile gibt es viele solcher Erweiterungen. In diesem Kapitel erhalten Sie Grundkenntnisse, mit deren Hilfe Sie CDI relativ einfach erweitern können. Darüber hinaus ermöglichen diese Grundlagen die Analyse und evt. Verbesserung von bestehenden CDI-Erweiterungen.Im weiteren Sinne handelt es sich bei portablen CDI-Erweiterungen um Aufsätze für CDI, welche nur auf Basis des APIs und SPIs von CDI implementiert sind. Sofern ein paar Feinheiten der CDI-Implementierungen beachtet werden, ist ein solcher Aufsatz mit jeder spezifikationskonformen Implementierung kompatibel. Im Optimalfall gilt dies auch für den Einsatz in Java EE6+ Servern. Hier kann jedoch die tiefe Integration von CDI und die unterschiedliche Auslegung von Teilen der Spezifikation eine zusätzliche Herausforderung darstellen. Bei den BDA-Regeln haben wir im Kapitel CDI und Java EE bereits verschiedene Interpretationen kurz kennengelernt.
Von einer portablen Erweiterung im engeren Sinne spricht man, wenn das Marker-Interface javax.enterprise.inject.spi.Extension implementiert wird, um bspw. während des Startprozesses der Applikation das Default-Verhalten abzuändern oder zu erweitern. Dieses Marker-Interface wird nur benötigt, um die entsprechenden Implementierungen für den ServiceLoader Mechanismus ( java.util.ServiceLoader ) zu konfigurieren. Implementierungen dieses Interfaces können beliebig viele CDI-Observer Methoden enthalten, welche vordefinierte Phasen des Container-Lifecycles überwachen. Abhängig von der Lifecycle-Phase können Sie die Standardkonzepte von CDI mit eigenen Mechanismen erweitern, um zusätzliche Anwendungsfälle möglichst komfortabel zu unterstützen.
4.1 Der Container-Lifecycle von CDI
Bevor wir mit einer konkreten Erweiterung beginnen, sehen wir uns den Container-Lifecycle im Detail an, da dieser die Basis für die erfolgreiche Implementierung von Extension-Klassen ist. Der Lifecycle besteht grundsätzlich aus zwei Hauptteilen. Zu Beginn steht der Containerstart, auch Bootstrapping-Prozess genannt, und am Ende der Containerstopp. Während des Containerstartes können Bean-Definitionen optional verändert, hinzugefügt oder entfernt werden. Dies ermöglicht die Integration mit beinahe beliebigen Frameworks, welche selbst keine explizite CDI-Unterstützung zur Verfügung stellen. Im nachfolgenden Teil lernen wir sämtliche Events des Lifecycles kennen.Wie eingangs erwähnt notifiziert der CDI-Container aktive Implementierungen von javax.enterprise.inject.spi.Extension durch CDI-Events. Für die Konfiguration und somit die Aktivierung eigener Extension -Klassen wird das Service-Loader Konzept, welches mit JDK6 eingeführt wurde, verwendet. Mit Hilfe der Klasse java.util.ServiceLoader werden im Falle von CDI alle konfigurierten Implementierungen des Markerinterfaces javax.enterprise.inject.spi.Extension durch den CDI-Container abgefragt, welche in einer oder mehreren Dateien namens /META-INF/services/javax.enterprise.inject.spi.Extension konfiguriert sind. Je Archiv, bspw. einer JAR-Datei, ist eine solche Konfigurationsdatei mit beliebig vielen Einträgen möglich. Ein Eintrag besteht aus dem vollständig qualifizierten Klassennamen einer Implementierung des zuvor erwähnten Markerinterfaces. Für jeden dieser Einträge erzeugt der CDI-Container eine Instanz, welche für die gesamte Applikation gültig ist. Hierbei handelt es sich um eine fixe Vorgabe. Die explizite Angabe eines Scopes ist dadurch nicht vorgesehen. Wie die Bezeichnung Markerinterface bereits vermuten lässt, sind durch javax.enterprise.inject.spi.Extension keine Methodensignaturen vorgegeben, wodurch nicht zu jedem Lifecycle-Event eine Methode zur Verfügung gestellt werden muss.
CDI-Events des Container-Lifecycles können in aktiven Extension -Klassen mit Hilfe von Observer-Methoden überwacht werden. Manche der Lifecycle-Events feuert der CDI-Container einmalig und andere für jedes gefundene Artefakt wie bspw. Beans. Die meisten dieser Events stehen bereits seit CDI 1.0 zur Verfügung. CDI 1.1 führt mit AfterTypeDiscovery , ProcessInjectionPoint und ProcessBeanAttributes drei zusätzliche Events ein, auf welche ebenfalls im nachfolgenden Abschnitt eingegangen wird.
Tipp: Selbst Extension s sind später in andere CDI-Beans injizierbar.
Durch diesen Ansatz ist es möglich während des Applikationsstartes Informationen zu sammeln,
um diese zu einem späteren Zeitpunkt zu verwenden.
Im nachfolgenden Teil erfahren Sie mehr über die einzelnen Lifecycle-Events und deren Einsatzgebiete.4.1.1 BeforeBeanDiscovery
Bevor mit der Verarbeitung der Bean-Kandidaten begonnen wird, feuert der CDI-Container das Event BeforeBeanDiscovery . Über dieses Event lassen sich Qualifier-, Scope-, Stereotyp- und Interceptor- Annotationen hinzufügen, welche nicht sämtliche Vorgaben der CDI-Spezifikation erfüllen. Darüber hinaus ist es sogar möglich für Klassen außerhalb von BDAs (Bean Deployment Archiv) einen sog. AnnotatedType , die Vorstufe von Managed-Beans, zu erzeugen und über dieses Event hinzuzufügen. Die später daraus resultierenden Beans unterscheiden sich kaum von Beans, welche regulär in einem BDA enthalten sind. Einen kleinen Unterschied gibt es jedoch auch hier durch BDAs. Für solche Bean-Kandidaten existiert keine beans.xml -Datei, wodurch nur global (Interceptor-,...) Konfigurationen, welche seit CDI 1.1 verfügbar sind, für die endgültigen Beans verwendbar sind. Abgesehen von der Einschränkung in Kombination mit BDAs ermöglicht die manuelle Registrierung von AnnotatedType s die Integration beliebiger Archive und Konfigurationsformate, um die standardmäßige Suche nach Bean-Kandidaten zu erweitern.4.1.2 ProcessAnnotatedType
Nach den manuellen Registrierungen via BeforeBeanDiscovery führt der CDI-Container einen Scan aller BDAs durch und erstellt für jede potentielle Bean-Klasse einen sog. AnnotatedType , welcher über ein Event mit dem Namen ProcessAnnotatedType gefeuert wird. Dieses Event ist das am häufigsten verwendete Event im Bootstrapping-Prozess, da portable CDI-Erweiterungen über dieses Event Bean-Definitionen verändern oder entfernen können. In IdeaFork nutzen wir dies, um im zweiten Teil dieses Kapitels bestimmte Typen für den CDI-Container zu deaktivieren. Eine solche Deaktivierung von Bean-Kandidaten ist durch den Aufruf der AnnotatedType -Methode #veto in einer Observer-Methode für das ProcessAnnotatedType -Event möglich.4.1.3 AfterTypeDiscovery
Dieses Lifecycle-Event ist seit CDI 1.1 verfügbar und wird gefeuert sobald sämtliche Typen registriert wurden. Die Methoden #getAlternatives , #getInterceptors und #getDecorators retournieren Listen der global aktivierten Klassen. Sowohl die Reihenfolge als auch der Inhalt der Listen kann zu diesem Zeitpunkt noch verändert werden. Globale Aktivierungen sind seit CDI 1.1 mit Hilfe der Annotation @Priority standardisiert. Wird bspw. ein Interceptor nur für ein BDA via beans.xml aktiviert, so ist dieser hier nicht enthalten.Mit #addAnnotatedType ist es möglich zusätzlich eigene Typen hinzuzufügen. Danach kennt der CDI-Container alle Definitionen, für welche auch tatsächlich Bean-Definitionen erstellt werden sollen. Alle via #veto exkludierten Typen sind spätestens jetzt nicht mehr in den internen Datenstrukturen des CDI-Containers vorhanden. Mit ProcessBeanAttributes , auf welches wir etwas später im Detail eingehen, ist seit CDI 1.1 ein solches Veto auch auf Basis der endgültigen Bean-Metadaten möglich.
4.1.4 ProcessInjectionPoint
Dieses ebenfalls mit CDI 1.1 eingeführte Event wird für jeden Injection-Point einer verwalteten Ressource aufgerufen. Hierzu zählen nicht nur CDI-Beans, sondern auch Artefakte wie bspw. EJBs. Dieses Lifecycle-Event stellt seit CDI 1.1 den ersten Schritt in der Phase zur Definition der finalen Managed-Bean-Metadaten dar. Davor war dies das nachfolgende Event namens ProcessInjectionTarget .ProcessInjectionPoint wird nicht nur für Injection-Points in registrierten Typen, sondern auch für Injection-Points aktiver Observer-Methoden und Producer erzeugt. Via #getInjectionPoint und #setInjectionPoint können Sie in speziellen Fällen den jeweiligen Injection-Point überprüfen bzw. ersetzen/dekorieren. Wird bei der Überprüfung eine (für die Applikation) ungülte Definition gefunden, so kann der Applikationsstart durch den Aufruf von #addDefinitionError(Throwable) abgebrochen werden.
4.1.5 ProcessInjectionTarget
Das Event ProcessInjectionTarget ist auf den Typ der Komponente typisiert und wird für jede Komponente (inkl. Java EE Komponenten) erzeugt, welche Injizierung unterstützt. javax.enterprise.inject.spi.InjectionTarget ist für die gesamte Erzeugung und Zerstörung, inklusive der Befüllung von Injection-Points, sowie für Aufrufe von Callback-Methoden (Post-Construct- und Pre-Destroy-Callbacks) verantwortlich. Da über dieses Event eine eigene Implementierung von InjectionTarget , bzw. ein Wrapper für die Default-Implementierung gesetzt werden kann, ist es auf einfache Weise möglich die erwähnten Prozesse zu erweitern oder anzupassen. Allerdings handelt es sich hierbei um sehr spezielle und tiefreichende Eingriffe, welche nur in seltenen Fällen erforderlich sind. Etwas interessanter ist hingegen die Auswertung der dazugehörenden AnnotatedType -Instanz, welche über #getAnnotatedType verfügbar ist. Über diese Instanz sind eigene Validierungen durchführbar und im Fehlerfall können Sie den Containerstart via #addDefinitionError(Throwable) abbrechen.4.1.6 ProcessBeanAttributes
Dieses Lifecycle-Event stellt das letzte mit CDI 1.1 eingeführte Event in unserer Beschreibung des Lifecycles dar. Es wird für jedes Bean, sowie Interceptoren und Decoratoren gefeuert und erlaubt mit Hilfe von #getBeanAttributes und #getAnnotated die bestehenden Metadaten zu überprüfen. Bei Bedarf ist es durch den Aufruf von #addDefinitionError(Throwable) möglich einen Fehler zu melden, wodurch wie üblich der Containerstart abgebrochen wird. In seltenen Fällen kann es erforderlich sein die bestehenden Bean-Attribute mit #setBeanAttributes zu verändern oder das gesamte Bean durch den Aufruf von #veto zu deaktivieren.4.1.7 ProcessProducer
Für jeden CDI-Producer, sowohl Producer-Methoden als auch Producer-Felder, wird dieses Lifecycle-Event gefeuert. Das Interface dieses Events erlaubt die Validierung des Producers und bei Bedarf kann mit dem Aufruf von #addDefinitionError(Throwable) der Applikationsstart abgebrochen werden. Darüber hinaus kann der Producer auch verändert werden. Anpassungen sollten jedoch nur mit einem Wrapper vorgenommen werden, welcher zumindest teilweise Aufrufe an die ursprüngliche Instanz delegiert. Um Fehler wie bspw. Memory-Leaks zu vermeiden, ist es empfehlenswert eine vollständige Implementierung des Producer -Interfaces mit Vorsicht umzusetzen.4.1.8 ProcessBean
Bevor ein aktives Bean effektiv registriert wird, feuert der CDI-Container das Event ProcessBean . Dies stellt den letzten Punkt vor der Registrierung des entsprechenden Beans dar. Zu diesem Zeitpunkt gibt es bereits die endgültige Managed-Bean Definition (Instanz von javax.enterprise.inject.spi.Bean<T> ). Somit besteht die Möglichkeit die finale Repräsentation der Managed-Bean Metadaten zu überprüfen und bei Bedarf den Containerstart via #addDefinitionError(Throwable) abzubrechen. Sind Sie nur an einem bestimmten Managed-Bean Typ interessiert, dann ist es möglich die konkreten Untertypen ProcessManagedBean , ProcessProducerMethod und ProcessProducerField anzugeben. Mit ProcessBean selbst können Sie alle Subtypen des Events überwachen.4.1.9 ProcessObserverMethod
Für Observer-Methoden gibt es ein separates Event namens ProcessObserverMethod , da für die Validierung solcher Methoden andere Metadaten erforderlich sind. Neben der Bean-Klasse und den Qualifier-Annotationen stehen auch sämtliche Informationen über die Methode selbst zur Verfügung. Das Grundkonzept entspricht dem von ProcessBean .4.1.10 AfterBeanDiscovery
Sobald der CDI-Container mit dem Verarbeitungsprozess inklusive der Validierung aktiver Managed-Beans fertig ist, wird das Event AfterBeanDiscovery gefeuert. Abgesehen von der bereits bekannten Methode #addDefinitionError(Throwable) stellt dieses Event Methoden für die manuelle Registrierung von eigenen Managed-Beans, Observer-Methoden und CDI-Context Implementierungen zur Verfügung. Wird ein eigenes Managed-Bean mit Hilfe der Methode #addBean hinzugefügt, so wird das zuvor vorgestellte Event ProcessBean gefeuert, bevor das Bean effektiv hinzugefügt wird. Zu beachten ist, dass es sich hierbei nicht um vollwertige Managed-Beans handelt, da bspw. keine Interceptoren unterstützt werden. Solche Beans sind somit mehr mit Producern vergleichbar und ermöglichen bspw. die Integration anderer Bean-Container.4.1.11 AfterDeploymentValidation
AfterDeploymentValidation stellt das letzte Event dar, bevor der CDI-Container vollständig gestartet ist. Zu diesem Zeitpunkt müssen sämtliche Validierungen, für welche der Container verantwortlich ist, abgeschlossen sein. Sollten Sie in einem Observer für dieses Event eine invalide Situation in der Applikation feststellen, so können Sie mit Hilfe der Methode #addDeploymentProblem das erfolgreiche Deployment der Applikation auch an diesem Punkt noch verhindern.Tipp: Das Event AfterDeploymentValidation wird oft zur manuellen Implementierung eines Startup-Events verwendet.
Dies funktioniert allerdings nur beschränkt und ermöglicht somit keine vollständig portable Implementierung.
Aus diesem Grund wurde die Annotation @Initialized mit CDI 1.1 eingeführt.
Seit CDI 1.1 ist somit der bevorzugte Ansatz zur Implementierung von portabler Initialisierungslogik ein Observer
mit dem Qualifier @Initialized(ApplicationScoped.class) .
4.1.12 BeforeShutdown
Dieses Lifecycle-Event wird gefeuert, bevor der CDI-Container gestoppt wird und ermöglicht bspw. die explizite Freigabe von geöffneten Ressourcen, bevor die Applikation beendet wird.4.2 Eigene CDI Erweiterungen entwickeln
Um eine eigene portable CDI-Erweiterung zu entwickeln, muss nicht jedes der zuvor vorgestellten Events überwacht werden. Die Stärke von CDI-Events, die absolute Entkoppelung zwischen Erzeuger und den Observer-Methoden, wird auch hier genutzt, um CDI-Erweiterungen möglichst leichtgewichtig zu halten. In IdeaFork möchten wir das ProcessAnnotatedType -Event dazu verwenden, um JPA-Entitäten für den CDI-Container zu deaktiviren. Wie bereits im Kapitel CDI und Java EE erwähnt, sollen Instanzen einer Klasse nur durch einen Container verwaltet werden, da es sonst zu Seiteneffekten mit den verschiedenen Proxy-Bibliotheken kommen kann. JPA-Entitäten können wir anhand der Annotation @Entity erkennen. Diesen Umstand nutzen wir, um Entity-Klassen für den CDI-Container unsichtbar zu machen. Hierfür legen wir die Klasse EntityVetoExtension an und implementieren das Marker-Interface javax.enterprise.inject.spi.Extension . Damit eine Extension-Klasse durch den CDI-Container gefunden und verwendet wird, müssen Sie diese in einer Datei namens /META-INF/services/javax.enterprise.inject.spi.Extension vollständig qualifiziert angeben. Listing Aktivierung einer portable CDI Erweiterung zeigt dies für unsere EntityVetoExtension .//content of /META-INF/services/javax.enterprise.inject.spi.Extension
at.irian.cdiatwork.ideafork.core.impl.infrastructure.EntityVetoExtension
public class EntityVetoExtension implements Extension {
protected void excludeEntityClasses(
@Observes ProcessAnnotatedType pat) {
if (pat.getAnnotatedType().isAnnotationPresent(Entity.class)) {
pat.veto();
}
}
}
In IdeaFork beschränken wir uns auf die Überprüfung von View-Controller-Beans und das Package für Services. Im vorherigen Kapitel haben wir teilweise EJBs als View-Controller verwendet. Dadurch haben wir uns in diesen Fällen ein eigenständiges transaktionales Service erspart. In komplexeren Applikationen ist es allerdings oftmals erforderlich, dass nur ein Teil der JSF Action-Methode(n) transaktional ausgeführt wird. Aus diesem und anderen Gründen, welche bereits im Kapitel CDI und Java EE erwähnt wurden, soll unsere erste Validierungsregel daher sicherstellen, dass EJBs nicht gleichzeitig mit @ViewController annotiert sind. Jeder Regelverstoß soll aufgezeichnet werden. Am Ende des Bootstrapping-Prozesses wollen wir im Fehlerfall den Applikationsstart abbrechen und alle Verstöße gesammelt als Grund für das Deployment-Problem angeben.
Da wir die effektiven Bean-Metadaten validieren möchten, verwenden wir in Listing Applikationsregeln validieren einen Observer für das Event ProcessManagedBean . Die Methode #getAnnotatedBeanClass gibt nicht direkt die Klasse selbst zurück, sondern eine Instanz vom Typ AnnotatedType . Über diese Instanz können wir nicht nur physisch verfügbare Metadaten überprüfen, sondern auch evt. dynamisch hinzugefügte, welche später effektiv für den CDI-Container sichtbar sind. Soll hingegen nur die jeweils physische Klasse und deren Metadaten geprüft werden, so können Sie die Methode #getJavaClass von AnnotatedType verwenden. Regelverstöße sammeln wir als Fehlermeldungen in einer Liste. In einem zweiten Observer, dieses Mal für das Event AfterDeploymentValidation , werten wir die gefundenen Verstöße aus. Über ProcessManagedBean könnten wir zwar ebenfalls den Startprozess abbrechen, jedoch wäre hier eine gesammelte Ausgabe aller Regelverstöße nicht möglich.
public class AppStructureValidationExtension implements Extension {
private static final Logger LOG = Logger.getLogger(/*...*/);
private List<String> violations = new ArrayList<String>();
public void validateArtifacts(
@Observes ProcessManagedBean pmb) {
if (pmb.getAnnotatedBeanClass()
.isAnnotationPresent(ViewController.class)) {
validateViewController(pmb.getAnnotatedBeanClass());
}
}
private void validateViewController(AnnotatedType annotatedType) {
for (Annotation annotation : annotatedType.getAnnotations()) {
if (annotation.annotationType()
.getPackage().getName().equals("javax.ejb")) {
this.violations.add(/*...*/); //violation message
}
}
}
public void checkAndAddViolations(
@Observes AfterDeploymentValidation afterDeploymentValidation) {
if (this.violations.isEmpty()) {
LOG.info(/*...*/); //success message
return;
}
StringBuilder violationMessage = new StringBuilder();
for (String violation : this.violations) {
violationMessage.append(violation);
}
this.violations.clear();
afterDeploymentValidation.addDeploymentProblem(
new IllegalStateException(violationMessage.toString()));
}
}
public class AppStructureValidationExtension implements Extension {
//...
public void validateArtifacts(
@Observes ProcessManagedBean pmb) {
//...
if (pmb.getAnnotatedBeanClass().getJavaClass()
.getPackage().getName().endsWith(".service")) {
validateService(pmb.getAnnotatedBeanClass());
}
if (pmb.getAnnotatedBeanClass()
.isAnnotationPresent(Singleton.class)) {
validateSingletonEjb(pmb.getAnnotatedBeanClass());
}
}
private void validateViewController(AnnotatedType annotatedType) {
//...
if (!annotatedType.getJavaClass().getName().endsWith("ViewCtrl")) {
LOG.warning(/*...*/);
}
}
private void validateService(AnnotatedType annotatedType) {
if (!annotatedType.isAnnotationPresent(Stateless.class)) {
this.violations.add(/*...*/);
}
}
private void validateSingletonEjb(AnnotatedType annotatedType) {
ConcurrencyManagement cmAnnotation =
annotatedType.getAnnotation(ConcurrencyManagement.class);
if (cmAnnotation == null ||
ConcurrencyManagementType.CONTAINER == cmAnnotation.value()) {
LOG.warning(/*...*/);
} else if (ConcurrencyManagementType.BEAN == cmAnnotation.value()) {
LOG.warning(/*...*/);
}
}
}
Den Erweiterungsmöglichkeiten sind nur wenige Grenzen gesetzt, wodurch CDI beinahe mit beliebigen Konzepten erweiterbar ist. Das nachfolgende Kapitel, zum Thema Apache DeltaSpike , illustriert weitere Möglichkeiten wie CDI portabel erweitert werden kann.