Teil 17: Backend meets Frontend: Trainer for kids 7

Dependency Injection in einer Vaadin-App

Sven Ruppert

© 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.

Lesetipp:

Grundkurs Docker: Eine praktische Einführung in die Welt der Container

Keine andere Technologie hat die IT in den letzten Jahren so geprägt wie Docker. Doch warum ist das so? Was macht Docker so besonders, wie funktioniert die Technologie unter der Haube und wie können Sie vom Trend profitieren? In unserem Grundkurs Docker lernen Sie anhand praktischer Beispiele, Docker und die Container-Technologie richtig einzusetzen.

Jetzt gratis auf JAXenter lesen!

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.

Backend meets Frontend

In der Artikelserie Backend meets Frontend stellt Sven Ruppert (Vaadin) Konzepte und Technologien rund um das UI-Framework Vaadin vor. Sein Fokus liegt dabei auf modernem Web-Design für Java-Backend-Entwickler.

Zum ersten Teil und damit dem Start der Tutorien rund um die UI-Entwicklung mit Java geht es hier entlang. Alle Teile der Serie Backend meets Frontend finden sich hier.

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!

Geschrieben von
Sven Ruppert
Sven Ruppert
Sven Ruppert arbeitet seit 1996 mit Java und ist Developer Advocate bei Vaadin. In seiner Freizeit spricht er auf internationalen und nationalen Konferenzen, schreibt für IT-Magazine und für Tech-Portale. Twitter: @SvenRuppert
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu: