Java CDI 2.0: Das bringt JSR365
Bald ist es soweit, das Release von CDI 2.0 (CDI = Contexts and Dependency Injection) nähert sich seinem Zieltermin im Juni diesen Jahres. Vor rund zwei Jahren, im September 2014, nahm die Expert Group unter der Leitung von Antoine Sabot-Durand, Spec-Lead des JSR365, ihre Arbeit auf. Derzeit ist CDI 1.2 die aktuelle Version der Spec, ein Minor Release zu CDI 1.1, welches in der aktuellen Java EE 7 Spec enthalten ist.
CDI hat sich zu einer grundlegenden Technologie innerhalb des Java Enterprise Stacks etabliert. Nahezu jede Spec innerhalb von Java EE bietet Integrationsmöglichkeiten zu CDI: Java Server Faces, EJB, JAX-WS, JAX-RS, die neue MVC Spec und etliche mehr. Heute wollen wir einen Blick auf die Neuerungen werfen, die CDI 2.0 mit sich bringt und Teil der JavaEE 8 Spec im nächsten Jahr werden soll. Wenn man den Gerüchten der letzten JavaLand-Konferenz Glauben schenken mag, so kann es gut sein, dass sich auch das CDI 2.0-Release bis kurz vor das JavaEE 8-Release verschiebt.
Bisher ist von CDI 2.0 nur der Early Draft 1 erschienen, der Early Draft 2 ist im Github-Repository [1] in Vorbereitung. Bei CDI 2.0 ist noch nicht final entschieden, welche Features in der endgültigen Spec enthalten sind. Auch die Ausprägung der Features kann sich in der nächsten Zeit ändern. Deshalb sollten auf Basis der folgenden Informationen keine kritischen Entscheidungen getroffen werden.
Asynchrone Events
Events ist ein sehr beliebtes Feature, das bereits seit CDI 1.0 häufig eingesetzt wird. Events erlauben eine Trennung von Komponenten. Events können an jeder beliebigen Stelle im Code erzeugt und gefeuert werden. Genau so kann auch jede Komponente, die den Event-Typ kennt, auf Events reagieren ohne Interfaces zu implementieren oder sich explizit in einer Listener-Registry zu registrieren, stattdessen übernimmt CDI das Management der ObserverMethods. Dies ist insbesondere praktisch, wenn Programmkomponenten auf Ereignisse reagieren müssen und lose gekoppelt sein sollen.
In den aktuellen CDI-Versionen wird das Eventhandling komplett synchron abgewickelt. Der Aufrufer wartet beim Feuern eines Events bis alle Event-Observer die Eventverarbeitung abgeschlossen haben. Mit CDI 2.0 kommen asynchrone Events. Sobald ein asynchrones Event gefeuert wird, kehrt die Kontrolle sofort zum Aufrufer zurück, die Events werden auf gesonderten Threads an die Observer-Methoden zugestellt und verarbeitet. Event-Observer können als asynchron deklariert werden und Events können asynchron gefeuert werden.
Dies bedeutet, dass sowohl Event als auch Observer als asynchron markiert werden.
Warum Double-Opt-in?
Die Frage nach der Gestaltung von Async Events hat innerhalb der Expert Group zu langen Diskussionen um die Gestaltungsmöglichkeiten geführt. Im Kern liegen Kompatibilität und Auswirkung. Anwendungen bestehen manchmal aus mehreren Code Modulen. Während ein Modul aktiv gepflegt wird, ist das andere Modul 3rd-Party, wird nicht mehr gepflegt oder muss aus Kompatibilitätsgründen eine frühere CDI-Version unterstützen. Wenn nur der Aufrufer über Asynchronizität bei Events entscheidet, sind die Event-Observer dieser Entscheidung ausgeliefert. Was aber, wenn der Observer asynchron verarbeitet werden möchte? Wie wirkt sich dies auf die anderen, synchronen Observer aus, die damit eventuell nicht umgehen können?
Würde Event-asynchronizität nur auf Event-Observer-Seite deklariert werden, so könnte dies auch zu unvorhergesehenem Verhalten bei den anderen Observern führen. Die aktuelle Grundannahme, dass Event-Observer sequenziell aufgerufen werden, gilt bei asynchronen Events nicht. Während synchrone Event-Observer der Reihe nach abgearbeitet werden, würden asynchrone Event-Observer parallel benachrichtigt und würden parallel zu den anderen Observern Aktivitäten ausführen. Dabei ist die Veränderung der Event-Nachricht wohl nur das geringste Problem.
CDI 2.0 geht mit Double-Opt-in bewusst einen anderen Weg. Synchrone Events bleiben mit Event.fire(…) und @Observes bestehen. Es werden keine asynchronen Event-Observer aufgerufen. Erst mit Event.fireAsync(…) und @ObservesAsync werden asynchrone Event-Observer aktiviert, jedoch keine synchronen Event-Observer.
Synchronisation mit asynchronen Event-Observern
Bei asynchroner Programmierung stellen sich plötzlich Fragen, die es bei synchroner Programmierung nicht gab. Die Synchronisation ist eine dieser Fragen, auf die es mit asynchronen Events auch eine Antwort gibt. Aufrufer von Event.fireAsync(…) erhalten eine Java 8 CompletionStage zur Synchronisation. Die CompletionStage erlaubt es, auf Fehler zu reagieren und mehrere Aufrufe zu verketten (Future chaining).
@Inject private Event<MyEventPayload> event; public void triggerEvent() { ComplectionStage<MyEventPayload> cs = event.fireAsync(new MyEventPayload()); cs.whenComplete((payload, throwable) -> { if(throwable != null){ // An error occured, handle the exception here. } else { // Event handling sucessful. } }); }
Fehlerzustände wie Exceptions bei der asynchronen Eventverarbeitung werden gesammelt und über eine CompletionException behandelt. Diese Exception ist über die CompletionStage erreichbar und über Lambda-Ausdrücke kann ein Exceptionhandler erzeugt werden. Der Beispielcode zeigt eine sehr einfache Behandlung über einen BiConsumer. Ist throwable nicht null, so ist ein Fehler bei der Verarbeitung aufgetreten, ansonsten war die Verarbeitung erfolgreich.
Vor einiger Zeit ist die Diskussion aufgekommen, ob CompletionStage der richtige Rückgabetyp ist oder ob nicht CompleteableFuture die bessere Wahl wäre. Das schöne an CDI 2.0 ist, dass sämtliche Informationen offen sind. Jeder kann sich an die Expert Group oder die Mailingliste [2] wenden und kann seine eigenen Vorschläge vorstellen.
Aktive Kontexte
Ein Kernkonzept von CDI sind die Kontexte, innerhalb derer Kontextinstanzen (Contextual instance) gehalten werden. Sobald ein Request beendet ist, ist auch sichergestellt, dass kein Anwendungscode für diesen Request mehr ausgeführt wird. Was aber, wenn die Eventverarbeitung asynchron erfolgt? Was geschieht, wenn der Request beendet ist, obwohl asynchrone Event-Observer noch aktiv sind und beispielsweise auf dem HTTP-Request Methoden aufrufen?
Um diesen Problemen aus dem Weg zu gehen sind während asynchroner Eventverarbeitung nur der Request- und der Application-Kontext aktiv. Der Request-Kontext verhält sich ähnlich wie mit asynchronen EJB-Methoden, der Kontext hat keinen Bezug mehr zum synchronen Request-Kontext und wird pro Thread neu erzeugt.
Asynchrone Events erlauben eine Parallelisierung der Event-Verarbeitung. Ideale Anwendungsfälle sind I/O oder CPU-intensive Aufgaben, die auf mehrere Threads verteilt werden. CDI löst einige Fallstricke der asynchronen Programmierung für den Anwender nicht. Dazu gehören beispielsweise immutable Eventnachrichten.
Sortierte Events
Bleiben wir doch noch ein wenig bei CDI-Events. Wenn heute CDI-Events im Einsatz sind, so kann man sich bezüglich der Ausführungsreihenfolge nie sicher sein. Die Reihenfolge hängt von vielen Faktoren ab. Maßgeblich ist der Typ der Event-Nachricht. Event-Observer reagieren nicht nur auf den Nachrichten-Typ selbst sondern auch auf Interfaces und übergeordnete Basisklassen.
Mit CDI 2.0 erhalten Observer-Methoden die Möglichkeit der Priorisierung. Die effektive Aufrufreihenfolge von CDI-Observer-Methoden wird erst beim Feuern der Nachricht bestimmt. Aus diesem Grund gibt es keine Sortierung im klassischen Sinn, sondern eine Priorität. Je kleiner der Wert ist, desto früher wird der Observer benachrichtigt, je größer, desto später. Mit Hilfe der @Priority-Annotation können einzelne Observer-Methoden früher oder später als andere Methoden aufgerufen werden. Methoden ohne eine @Priority-Annotation erhalten eine Standard-Priorität von 2500.
Warum 2500? Die Prioritäten orientieren sich an javax.interceptor.Priority. In dieser Klasse ist bereits ein Bereich von 2000 bis 2999 für die Applikation vorgesehen. So fiel die Entscheidung, die Mitte zu nutzen. Dies bedeutet auch, dass Observer-Methoden mit einer Priorität von 2499 vor allen Observer-Methoden benachrichtigt werden. Observer mit einer Priorität von 2501 werden nach allen anderen Observer-Methoden aufgerufen, sofern alle anderen Observer-Methoden den Defaultwert nutzen. Es bleiben 998 Werte, die für die Priorisierung genutzt werden können. Observer-Methoden mit gleichen @Priority-Werten führen erneut zu einem undefinierten Verhalten im Bezug auf Reihenfolge. Events im SPI, innerhalb von Erweiterungen, können @Priority ebenfalls verwenden.
Metadata-Builder API
Jeder, der schon mal eine CDI-Extension implementiert hat, ist an SPI-Events und deren Typen vorbeigekommen. SPI-Events erlauben es, CDI in seiner Funktionalität zu erweitern, indem neue Beans, Observer-Methoden, Interceptoren uvm. registriert werden können. Ein CDI-Bean benötigt eine Implementierung von Bean, die nicht trivial ist. Die Referenzimplementierung Weld und das Erweiterungsframework Deltaspike bieten bereits ein API um Beans, InjectionPoints und weitere CDI-Typen zu konfigurieren. CDI 2.0 standardisiert das Metadata-Builder-API und bringt folgende Builder mit sich:
- AnnotatedTypeBuilder
- InjectionPointBuilder
- BeanAttributesBuilder
- BeanBuilder
- ObserverMethodBuilder
Fluent API – ungewohnt, aber fließend.
Mit diesen Buildern lassen sich während der SPI-Events neue Beans erzeugen. Das CDI-Metadata-API folgt einem Builder/Configurator-Pattern. Ein Builder ist die zustandsbehaftete Instanz. Diese gibt über den Configurator die Möglichkeit, den Builder entsprechend zu konfigurieren. Sobald der Builder durch den Code konfiguriert wurde, erzeugt ein Aufruf der build()-Methode eine neue Instanz, die zum CDI-Container hinzugefügt werden kann.
CDI 2.0 beschreitet bei dem Metadata-Builder-API einen neuen Weg. Statt nur auf Builder zu setzen, liefern die SPI-Events direkt add...-Methoden wie addBean, die einen BeanConfigurator zurückgeben. Dieser Konfigurator wird mit den entsprechenden Attributen aufgerufen und nachdem das Event bearbeitet wurde, wird das Bean hinzugefügt. Diese Vorgehensweise funktioniert für ObserverMethod, InjectionPoint, BeanAttributes und AnnotatedType. Die neue Art Erweiterungen zu konfigurieren ist ungewohnt, erlaubt jedoch eine fließende Verwendung (Fluent API).
Java SE
CDI lässt sich innerhalb von Containern und mit JavaSE verwenden. Bislang bringt jede Implementierung ihren eigenen Mechanismus mit. CDI 2.0 standardisiert das Java SE-Bootstrapping. Bereits im Early Draft 1 wurde ein API vorgeschlagen, welches den Container-Start in nur drei Code-Zeilen ermöglicht.
public static void main(String[] args) { try(CDI<Object> cdi = CDI.current()) { cdi.select(MyApp.class).get().runMyApplication(); } } public class MyApp{ public void runMyApplication(){ // ... } }
Die CDI-Klasse implementiert AutoCloseable und kann somit im try-with-resources-Konstrukt verwendet werden. Nach der Ausführung wird die CDI-Instanz heruntergefahren.
Damit ist der Grundstein für JavaSE gelegt, jedoch fehlt das entscheidende API um Kontexte zu starten und zu stoppen. Das Erweiterungsframework Deltaspike bietet dieses API bereits heute an. Es ist also wahrscheinlich, dass dieses API standardisiert wird, da die Deltaspike-Macher auch in der Expert Group vertreten sind. Deltaspike selbst ist eine Sammlung portabler CDI-Extensions, die beim Einsatz von CDI wiederkehrende Aufgaben lösen.
Java 8
CDI 2.0 setzt – wie alle zukünftigen JavaEE 8 Erweiterungen – auf Java 8. Neue APIs sollen von Java 8 profitieren, bestehende APIs sollten geprüft werden, ob diese mit Java 8 erweitert werden können. Asynchrone Events verwenden CompletionStage zur Synchronisation von Events. Ein weiteres Feature das mit CDI 2.0 Einzug hält ist die Unterstützung von wiederholten Annotationen für Qualifier:
public interface MyProducer { @Produces @ApplicationScoped @Language("German") @Language("English") public Translator createTranslator() { ... } }
Ansonsten hält sich die weitere Unterstützung für Java 8-Features in Grenzen. Es gibt Überlegungen, beim Metadata-Builder API Lambda-Ausdrücke für Observer-Methoden und Interceptoren zu erlauben, aber diese Entwürfe sind noch in Diskussion.
Aufteilung der Spec in Core, JavaSE und JavaEE
EJB ist als Vorreiter mit der Idee gestartet, Teile der Spec von EJB 2.x optional zu gestalten. CDI 2.0 folgt mit einer ähnlichen Idee. CDI lässt sich mit etlichen Java EE-Technologien nutzen. Entsprechend ist auch das TCK (Technology Compatibility Kit) aufgesetzt, welches eine CDI-Implementierung auf die Spec-Kompatibilität hin prüft. Bisher sind nur Weld, OpenWebBeans und CanDI CDI-kompatibel. Deshalb teilt CDI 2.0 die Spec in einen Java SE- und Java EE-Teil auf. Mit diesem Schritt soll die Einstiegshürde für weitere DI-Frameworks gesenkt werden. DI-Frameworks können so nur den Java SE-Teil implementieren und sind CDI Java SE-kompatibel. So sind aus 92 Seiten Spec rund 200 Seiten geworden.
… und vieles mehr …
CDI 2.0 bringt viele weitere Änderungen mit, die sich derzeit teilweise noch im Entwurf und in der Diskussion befinden. Darunter findet sich die Ausnahme zur Regel für non-private non-static final-Methoden. Diese Regel besagt, dass public, protected und package default Instanz-Methoden von CDI-Beans nicht final sein dürfen. Diese Regel existiert seit CDI 1.0. Der Grund für die Regel ist, dass CDI-Beans in Proxy-Objekte eingepackt werden um dem Aufrufer die korrekte Instanz bereitzustellen. Proxies folgen den gleichen Gesetzmäßigkeiten wie regulärer Java-Code was im Fall von final-Methoden bedeutet, dass diese nicht überschrieben werden dürfen. Mit Java 7 sind bereits Typen (z. B. ConcurrentHashMap.replaceNode) hinzugekommen, die finale Methoden anbieten. Nutzer dieser Typen konnten ihre Anwendungen ab Java 7 nicht länger ohne Änderung starten. Mit der Ausnahmeregelung können diese CDI-Anwendungen mit neueren Java-Versionen gestartet werden. Die Aufrufe erfolgen stets am Proxy. Da der Methodenaufruf an einer nichtüberschriebenen Methode stattfindet und das Proxy-Objekt nicht mit Daten initialisiert ist, führen diese Aufrufe zu Fehlern. Aufrufe zu den anderen Methoden sind dagegen erfolgreich.
Mit CDI 2.0 kommen Annotationsliterale für CDI-Annotationen. Damit ist es nun nicht länger erforderlich, eigene Klassen zu erzeugen, sondern die mitgelieferten Instanzen zu verwenden.
Bisher:
abstract class AbstractNamedLiteral extends AnnotationLiteral<Named> implements Named { } beanManager.getBeans(MyBean.class, new AbstractNamedLiteral() { @Override public String value() { return "beanName"; } });
Ab CDI 2.0:
beanManager.getBeans(MyBean.class, NamedLiteral.of("beanName"));
Ebenfalls zur Diskussion steht der neue Bean-Discovery-Modus scoped. Bei diesem Modus werden nur Beans aktiviert, die mit Bean-definierenden Annotationen versehen sind. Das bedeutet, dass nur Beans als solche erkannt werden die z. B. mit @RequestScoped, @Interceptor, @Stereotype annotiert sind.
Die Spec wurde an vielen Stellen genauer gefasst und mehrdeutige Formulierungen wurden eindeutiger gestaltet. Wer eine Liste aller Änderungen für CDI 2.0 sucht, wird wohl auf das Commitlog des CDI Spec Git repositories [1] zurückgreifen müssen. Es gibt zwar ein Jira-Projekt [3], aber auch dort gestaltet sich die Suche nach einer einheitlichen Sicht schwierig, da Tickets nur den Meilensteinen (Diskussion, Early Draft Review) zugewiesen sind. Es bleibt abzuwarten, wie sich die Releasetermine von CDI 2.0 und Java EE8 entwickeln und wann letztlich die Anwender in den Genuss von CDI 2.0 kommen.