Dependency Injection in einer Vaadin-App

© Shutterstock / HelenField
Dependency Injection kann eine kniffelige Angelegenheit sein. Bei der Umsetzung helfen deswegen Frameworks wie Weld oder DDI. Wie das Ganze bei einem Vaadin-Projekt funktionieren kann, zeigt dieser Teil der Kolumne „Backend meets Frontend“. Außerdem geht es um die Frage, wie einfach oder eben schwierig es ist, das DI-Framework nachträglich zu wechseln.
In diesem Teil wollen wir uns damit beschäftigen, wie sich die Verwendung von Dependency Injection auf Vaadin-Projekte auswirkt. Hierzu werden wir uns zwei Vertreter der DI-Frameworks ansehen: CDI und DDI. Wer ein wenig mehr zu dem Thema lesen möchte, dem empfehle ich die Analyse von zwei weiteren Frameworks inklusive deren Eigenheiten. Zum einen BoonDI und Dagger und zum anderen ein wenig über CDI an sich. Auch wenn es eventuell neuere Versionen der Frameworks gibt, zeigen die beiden Artikel deutlich, welche Dinge bei DI-Frameworks in die Quere kommen können.
Um die Beispiele zur Dependency Injection unabhängig zu zeigen und Möglichkeiten zum Vergleich zu geben, habe ich mich dazu entschlossen die jeweiligen Experimente in eigene Module auszulagern. Demnach haben wir nun drei Module. Im ersten zeige ich einen Bootstrap mit CDI basierend auf Weld, im zweiten einen Bootstrap basierend auf DDI und zum Schluss die Modifikationen im bisherigen Projekt. Das letzte Modul wird dann die Grundlage für die nächsten Teile dieser Serie bilden.
CDI mit Weld
Dependency Injection gibt es in verschiedenen Versionen und Varianten. Es gibt einige Frameworks, die dabei helfen und eine Variante von Dependency Injection implementieren, mit einigen Unterschieden. Ich werde nicht im Detail auf alle Unterschiede eingehen können, wir werden jedoch einige zu Gesicht bekommen. Ziel ist es, herauszuarbeiten, wo und wie Dependency Injection in einer Vaadin-Applikation sinnvoll sein kann.
In der ersten Variante sehen wir uns Weld an. Weld ist die Referenzimplementierung für CDI. Hier kommt die Version 3.0.1 zum Einsatz, in der SE-Version. An dieser Stelle werde ich absichtlich
ausschließlich die SE-Version verwenden, da hier recht viele Dinge aus der EE-Welt weder gebraucht werden noch helfen würden. Um mit Weld beginnen zu können, fügen wir dem Modul die Abhängigkeit hinzu.
<dependency> <groupId>org.jboss.weld.se</groupId> <artifactId>weld-se-core</artifactId> <version>3.0.1.Final</version> </dependency>
Nun können wir den Container initialisieren und der Anwendung zur Verfügung stellen. Dies findet alles in der Klasse statt, in der wir den Undertow starten. Hier ist es die Klasse MainCDI
. Um später auf den Container zugreifen zu können, bin ich der Einfachheit halber einfach dazu übergegangen, ein statisches Attribut zu deklarieren.
public static WeldContainer weldContainer;
Dieses Attribut setzen wir innerhalb der main-Methode, noch bevor wir den Undertow initialisieren. Beim Erzeugen der Instanz wird das selbständige Suchen nach zu verwaltenden Klassen deaktiviert. Das hat den Grund, dass dadurch der Vorgang zur Erzeugung selbst schneller ist. Wenn man dies nicht macht, wird zu Beginn der gesamte Klassenpfad durchsucht. Nachfolgend werden lediglich die Packages rekursiv abwärts aktiviert, in denen die zu verwaltenden und von uns erstellten Klassen zu finden sind.
weldContainer = new Weld() .disableDiscovery() .addPackage(true, MainCDI.class) .addPackage(true, UIFunctions.class) .initialize();
Jetzt sind wir fertig mit den Vorbereitungen und können den Container verwenden. Als erstes wird das Servlet selbst als vom Weld-Container verwaltete Instanz realisiert. Um das zu verdeutlichen, ist eine Methode dem Servlet hinzugefügt worden, die mittels @PostConstruct
annotiert worden ist. Das hat lediglich den Sinn zu zeigen, dass der Lebenszyklus funktioniert.
@WebServlet(value = "/*", loadOnStartup = 1) @VaadinServletConfiguration(productionMode = false, ui = MainUI.class ) public class MainServlet extends VaadinServlet { @PostConstruct private void postConstruct(){ System.out.println("CDi activated for MainServlet "); } }
Als nächstes müssen wir noch die Stelle finden, in der wir uns einhängen können, um die Instanz dieses Servlets zu erzeugen. Der Undertow bietet die Möglichkeit, bei der Definition der Servlet-Konfiguration eine Factory mit anzugeben. Und das ist genau der Punkt, an dem die Verbindung zum Weld-Container hergestellt werden kann.
static DeploymentInfo addServlet(DeploymentInfo deploymentInfo , Class<? extends Servlet> servletClass , String filterMapping) { return deploymentInfo .addServlets( servlet( servletClass.getSimpleName() , servletClass, new ServletInstanceFactory(servletClass) //activate CDI on Servlet ) .addMapping(filterMapping) ); }
Der Methode servlet(..)
wird hier einer Instanz vom Typ InstanceFactory
übergeben. Dieses Interface muss selbst implementiert werden, hier in der Klasse ServletInstanceFactory
.
public class ServletInstanceFactory implements InstanceFactory<Servlet> { private final Class<? extends Servlet> servletClass; public ServletInstanceFactory(Class<? extends Servlet> servletClass) { this.servletClass = servletClass; } @Override public InstanceHandle<Servlet> createInstance() throws InstantiationException { return new InstanceHandle<Servlet>() { @Override public Servlet getInstance() { return MainCDI.weldContainer.select(servletClass).get(); } @Override public void release() { //release ??? } }; } }
Die maßgebliche Stelle ist MainCDI.weldContainer.select(servletClass).get();
. Hier wird der Weld-Container geholt und mittels select(..).get()
eine Instanz erzeugt. Diese Instanz verwaltet der Container und sie durchläuft alle definierten Lebenszyklen. Verdeutlicht haben wir dies durch die Ausgabe auf der Kommandozeile aus der Methode heraus, die mit @PostConstruct
annotiert worden ist.
Nun haben wir ein Servlet, in dem wir mittels @Inject
arbeiten können. Nur haben wir hier leider keine Verwendung dafür. Gehen wir also zum nächsten Punkt. Das Vaadin-Servlet ist mit der Klasse verbunden, die uns das UI abbildet. In diesem Fall ist das die Klasse MainUI
. Diese Instanz wird nicht vom Weld-Container verwaltet. Demnach können wir hier noch nicht mit @Inject
arbeiten. Bei Vaadin kann man aber in den Erzeugungsprozess eingreifen. Dazu müssen wir das Servlet ein wenig erweitern.
@Override protected VaadinServletService createServletService( final DeploymentConfiguration config) throws ServiceException { final CDIVaadinServletService service = new CDIVaadinServletService(this, config); service.init(); return service; }
Die Methode createServletService(..)
wird hier überschrieben, damit wir eine eigene Implementierung vom Typ VaadinServletService
verwenden können und alle programmatischen Anfragen dorthin delegiert werden. Der VaadinServletService
wird unter anderem dazu verwendet, die Instanz der UI-Klasse zu erzeugen. Bei der Implementierung des Interfaces müssen zwei Methoden berücksichtigt werden. Die Methode addSessionDestroyListener(..)
wird einfach mit einer Dummy-Implementierung versehen, da wir sie in diesem Beispiel nicht benötigen. Wichtig für uns ist an dieser Stelle die Implementierung der Methode addSessionInitListener(..)
. In dem übergebenen Event ist die Klasse enthalten, die mittels Annotation @VaadinServletConfiguration
an das Servlet gebunden worden ist. Diese Klasse verwenden wir, um eine Instanz vom Weld-Container zu bekommen. Nun können wir auch in der UI-Klasse mittels @Inject
arbeiten.
public class CDIVaadinServletService extends VaadinServletService { public CDIVaadinServletService(VaadinServlet servlet , DeploymentConfiguration deploymentConfiguration) throws ServiceException { super(servlet , deploymentConfiguration); addSessionInitListener(event -> event.getSession().addUIProvider(new DefaultUIProvider() { @Override public UI createInstance(final UICreateEvent event) { return MainCDI.weldContainer.select(event.getUIClass()).get(); } })); addSessionDestroyListener(event -> { }); } }
Jetzt haben wir alle wichtigen Elemente aufgebaut und können alle weiteren Elemente vom Weld-Container verwalten lassen. Als erstes Beispiel nehmen wir LoginComponent
. In der UI-Klasse ersetzen wir die Implementierung der Methode login()
wie folgt:
@Inject private Instance<LoginComponent> loginComponentInstance; private LoginComponent login() { return loginComponentInstance.get(); }
Innerhalb der LoginComponent
ersetzen wir auch wieder die direkten Erzeugungen durch Deklarationen.
// private final Supplier<MainView> mainViewSupplier = MainView::new; @Inject private Instance<MainView> mainViewSupplier; @Inject private LoginService loginService; // private final LoginService loginService = new LoginServiceShiro(); @Inject private UserService userService; // private final UserService userService = new UserServiceInMemory();
Aber gehen wir noch einen Schritt weiter. Wenn man sich mit dem Thema Dependency Injection im Allgemeinen beschäftigt, stehen nach dem ersten Einarbeiten einige Fragen im Raum. Wie beeinflusst das gewählte Framework die technische Architektur? Ebenfalls stellt sich die Frage nach der Abhängigkeit zu diesem Framework. Wie aufwendig ist es zu einem anderen Framework zu wechseln? Das wollen wir mit unserem Projekt einmal ausprobieren.
CDI mit DDI
Als nächstes verwenden wir ein anderes Open-Source-Projekt: Dynamic Dependency Injection. Hier wird ein etwas anderer Ansatz gewählt. Im Gegensatz zu CDI ist die Konfiguration nicht statisch in einem Container verpackt. Außerdem gibt es auch andere Möglichkeiten die Implementierung zur Laufzeit zu wählen, ohne dass es in XML-Dateien oder mittels Annotation erfolgen muss. Das Projekt selbst ist übrigens eines der wenigen, das über eine Mutation-Test-Abdeckung von über 95 Prozent verfügen. Wer darüber mehr erfahren möchte, sollte sich die Webseite zum Projekt ansehen. Das Beispiel hierzu befindet sich im Maven-Modul DDI. Als erstes fügen wir wieder die Abhängigkeit zum DI-Framework zur pom.xml hinzu.
<dependency> <groupId>org.rapidpm.dynamic-cdi</groupId> <artifactId>rapidpm-dynamic-cdi-modules-core</artifactId> <version>0.9.0</version> </dependency>
Nachfolgend beginnen wir mit dem Bootstrap. Dies sollte immer noch in der main-Methode stattfinden, in der wir den Undertow initialisieren. Der Vorgang ist natürlich ein wenig anders als bei Weld. Auch hier werden nur die notwendigen Packages aktiviert.
DI.activatePackages(MainDDI.class); DI.activatePackages(UIFunctions.class);
Als nächstes werden die Zeilen im Servlet ausgetauscht. Es ändert sich lediglich die verwendete Implementierung des benötigten VaadinServletService. Hier ist es die Klasse DDIVaadinServletService
.
@Override protected VaadinServletService createServletService(final DeploymentConfiguration deploymentConfiguration) throws ServiceException { final DDIVaadinServletService service = new DDIVaadinServletService(this, deploymentConfiguration); service.init(); return service; }
In der Implementierung DDIVaadinServletService
ändert sich nur die Methode addSessionInitListener(..)
. Die relevante Zeile ist DI.activateDI(event.getUIClass())
.
addSessionInitListener(event -> event.getSession().addUIProvider(new DefaultUIProvider() { @Override public UI createInstance(final UICreateEvent event) { return DI.activateDI(event.getUIClass()); } }));
Zum Erzeugen der Servlet-Instanz wird auch hier wieder eine Factory erstellt. Die einzige Änderung ist hier in der Methode getInstance()
.
@Override public InstanceHandle<Servlet> createInstance() throws InstantiationException { return new InstanceHandle<Servlet>() { @Override public Servlet getInstance() { return DI.activateDI(servletClass); } @Override public void release() { //release ??? } }; }
Soweit ist der Bootstrap nun umgebaut. Wie sieht es in den UI-Komponenten aus? In der LoginComponent
wird die Zeile entfernt, in der wir den Proxy auf die MainView
definieren: @Inject private Instance mainViewSupplier;
. Stattdessen verwenden wir im ClickListener direkt den folgenden Aufruf: current.setContent(DI.activateDI(MainView.class));
. Die Definitionen der beiden verwendeten Services bleiben erhalten.
@Inject private LoginService loginService; @Inject private UserService userService;
Und so werden nun alle Referenzen bearbeitet.
Fazit
Bei diesem Beispiel haben wir deutlich gesehen, dass es unterschiedliche Aspekte gibt, die wir bei Dependency Injection berücksichtigen müssen. Um eine Antwort auf die Frage nach der Abhängigkeit zu einem bestimmten Framework zu geben, werden wir die relevanten Codeteile in verschiedene Module extrahieren müssen. Die technische Umsetzung werden wir im nächsten Teil ansehen und besprechen. Den aktuellen Quelltext unseres Projekts findet ihr auf GitHub. Bei Fragen und Anregungen einfach melden unter sven@vaadin.com oder per Twitter @SvenRuppert.
Happy Coding!
Hinterlasse einen Kommentar