Backend meets Frontend - Teil 2: Plain Undertow

Web-UIs mit Java erstellen: So geht’s mit Vaadin im Core Servlet Container

Sven Ruppert

© Shutterstock / HelenField

In der Artikelserie Backend meets Frontend stellt Sven Ruppert Konzepte und Technologien rund um das UI-Framework Vaadin vor. Nachdem im ersten Teil das Aufsetzen einer Vaadin-App in Wildfly Swarm erarbeitet wurde, geht es dieses Mal um das Deployment in einem Undertow Servlet Container.

Plain Undertow – Vaadin im Core Servlet Container

Im ersten Teil dieser Serie haben wir gesehen, wie wir eine minimale Vaadin App erstellen und diese in Wildfly Swarm deloyen können. Wildfly Swarm basiert auf dem Web Server Undertow und bietet einige zusätzliche Java-EE-Features, die bei Bedarf per Dependency dazu geholt werden können.

Heute wollen wir uns ansehen, wie wir all das ohne Wildfly Swarm mit einem Core Undertow realisieren können. Ziel ist es hier, die Round-Trip-Zeiten in der Entwicklung zu verkürzen und damit auch die Zeiten, die eine CI/CD-Strecke benötigt.

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. Bisher erschienen:

Ein „Hello World“-Servlet

Beginnen wir mit den technischen Dingen. Um mit der Erstellung einer Webanwendung mit Vaadin zu beginnen, benötigen wir ein Servlet. Als Servlet-Container wollen wir hier Undertow nutzen, andere sind aber genauso gut verwendbar.

Nun gibt es verschiedene Wege, eine Webanwendung in einem Servlet-Container laufen zu lassen, oder besser gesagt dort zu „deployen“. Der offizielle Vaadin-Weg ist hier beschrieben. In unserem Beispiel wollen wir jedoch den Servlet Container Undertow eingebettet laufen lassen.

Undertow: Was ist das?

Undertow ist aus dem Hause Red Hat und kommt im Java Application Server Wildfly zum Einsatz. Wenn wir mit einem „Hello World“ beginnen möchten, erstellen wir zuerst ein Projekt. Ich verwende Maven in diesem Beispiel und füge die Definition zur Verwendung von Java 8 hinzu.

  <properties>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
  </properties>

Um nun Undertow verwenden zu können, fehlen noch die Abhängigkeiten zu Undertow selbst und zum Servlet API.

 
    <!--API´s-->
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>javax.servlet-api</artifactId>
      <version>3.1.0</version>
      <scope>provided</scope>
    </dependency>

    <!--Infrastructure-->
    <dependency>
      <groupId>io.undertow</groupId>
      <artifactId>undertow-core</artifactId>
      <version>1.4.16.Final</version>
    </dependency>

    <dependency>
      <groupId>io.undertow</groupId>
      <artifactId>undertow-servlet</artifactId>
      <version>1.4.16.Final</version>
    </dependency>

Soweit präpariert, können wir beginnen und erstellen unsere erste Klasse mit dem Namen „HelloWorldMain“

 
public class HelloWorldMain {
  public static void main(String[] args) {
    Undertow server = Undertow
        .builder()
        .addHttpListener(8080, "localhost")
        .setHandler(new HttpHandler() {
          @Override
          public void handleRequest(HttpServerExchange exchange) 
              throws Exception {
            exchange.getResponseHeaders()
                    .put(Headers.CONTENT_TYPE, "text/plain");
            exchange.getResponseSender()
                    .send("Hello World");
          }
        }).build();
    server.start();
    server.getListenerInfo().forEach(System.out::println);
  }
}

Dank der Lambdas aus Java 8 können wir den obigen Quelltext ein wenig kompakter schreiben.

 
public class HelloWorldMain {
  public static void main(String[] args) {
  public static void main(String[] args) {
    Undertow server = Undertow
        .builder()
        .addHttpListener(8080, "localhost")
        .setHandler(exchange -> {
          exchange.getResponseHeaders()
                  .put(Headers.CONTENT_TYPE, "text/plain");
          exchange.getResponseSender()
                  .send("Hello World");
        }).build();
    server.start();
    server.getListenerInfo().forEach(System.out::println);
  }
}

Was passiert hier? In der Methode main beginnen wir mit dem Undertow.Builder, der es uns ermöglicht, die zu erzeugende Instanz des Servlet-Containers zu konfigurieren.
Es wird ein HTTP Listener hinzugefügt, der auf dem Port 8080 lauscht und an die IP-Adresse localhost gebunden wird.

Um nun Requests zu verarbeiten, registrieren wir als nächstes einen HttpHandler und reagieren auf einen Request mit der Antwort „Hello World“. Nun können wir die Instanz starten und testen.

Der erste Test

Wenn wir die Klasse mit der main-Methode starten, können wir das Ergebnis „Hello World“ mittels Browser überprüfen. Hierzu rufen wir die Adresse http://localhost:8080/ auf und, welch Überraschung, es erscheint als Ergebnis ein „Hello World“.

Nun stellen sich einige weitere Fragen. Die erste könnte sein: Wie teste ich mit jUnit mein „Hello World“ ? In diesem Artikel verwenden wir  jUnit 4, da es wohl derzeit noch weiter verbreitet ist als jUnit 5. Außerdem streben wir an, dass der Servlet Container für einen Test jedesmal neu gestartet wird. Das Vorgehen ist recht einfach, da wir ja schon über einen vollständigen Initialisierungsmechanismus verfügen.

In der Methode setUp wird der Servlet-Container gestartet und in der Methode tearDown wieder gestoppt. Um das zu erreichen, muss nicht viel passieren, wie das folgende Listing zeigt.

 
  @Before
  public void setUp() throws Exception {
    HelloWorldMain.main();
  }

  @After
  public void tearDown() throws Exception {
    //HelloWorldMain.shutdown(); //NotYetImplemented
  }

Und fertig sind wir! Es fehlt nun noch der Test an sich. Hierfür fügen wir als erstes die Abhängigkeit zu dem hier verwendeten Projekt OkHttP her.

 
    <dependency>
      <groupId>com.squareup.okhttp3</groupId>
      <artifactId>okhttp</artifactId>
      <version>3.8.0</version>
      <scope>test</scope>
    </dependency>

Da ich persönlich kein Freund langer Namen bei Testmethoden bin (stattdessen lieber kurze eindeutige Test schreiben), nenne ich den ersten Test „test001“.

 
    Request request = new Request.Builder()
        .url("http://127.0.0.1:8080/")
        .build();

    OkHttpClient client = new OkHttpClient();
    Response response = client.newCall(request).execute();

    Assert.assertNotNull(response);
    ResponseBody body = response.body();
    Assert.assertNotNull(body);
    Assert.assertEquals("Hello World", body.string());

Gehen wir im Einzelnen durch, was hier abläuft. Als erstes erzeugen wir einen Request, der lediglich die URL http://127.0.0.1:8080/ aufrufen wird. Der Request selbst wird an der Stelle noch nicht ausgeführt.

Das Ausführen der Requests erfolgt durch eine Instanz der Klasse OkHttpClient. Die Methode newCall(..) bekommt das Ziel, und der Aufruf execute() führt den Request dann aus. Die Methode gibt eine Instanz der Klasse Response zurück, in der die jeweils vorhandenen Daten, die der Server zurückgesendet hat, enthalten sind. In unserem Fall sind wir lediglich am Inhalt selbst interessiert. Diesen holen wir aus dem body der Antwort heraus.

Alles zusammen läuft (auf meinem Laptop/MacBookPro) in ca. 880 ms durch. In dieser Zeit enthalten ist der Start der JVM, genauso wie der Start des hier verwendeten Servlet-Containers. Zum Vergleich: Im ersten Teil dieser Serie hat der Initialisierungszyklus mit Wildfly-Swarm ca. 6 Sekunden auf der selben Maschine benötigt.

Es werde bunt

Da wir nun ein Servlet inklusive eines Servlet-Containers zusammen haben, können wir uns jetzt um das UI kümmern. Um Vaadin zu verwenden, benötigen wir die nachfolgend aufgelisteten Abhängigkeiten.

 
<!--Vaadin -->
    <dependency>
      <groupId>com.vaadin</groupId>
      <artifactId>vaadin-server</artifactId>
      <version>${vaadin.version}</version>
    </dependency>
    <dependency>
      <groupId>com.vaadin</groupId>
      <artifactId>vaadin-themes</artifactId>
      <version>${vaadin.version}</version>
    </dependency>

    <dependency>
      <groupId>com.vaadin</groupId>
      <artifactId>vaadin-client-compiled</artifactId>
      <version>${vaadin.version}</version>
    </dependency>

Wie im ersten Teil der Serie beschrieben, benötigen wir ein Servlet und ein UI für unsere Vaadin App. Vaadin selbst bietet ein Servlet an, das die Grundlage der Vaadin-basierten Anwendung darstellt. Hier in unserem Fall reicht es aus, wenn wir von VaadinServlet ableiten und die Verbindung zu unserem UI herstellen.

 
  @WebServlet
  @VaadinServletConfiguration(productionMode = false, ui = MyUI.class)
  public static class MyProjectServlet extends VaadinServlet {}

Mittels der Annotation @VaadinServletConfiguration(..) verknüpfen wir über das Attribut ui eine Klasse, die von com.vaadin.ui.UI ableitet und uns die Möglichkeit gibt, mit der Definition des UI zu beginnen. Wir werden das in unserem Fall als erstes wieder mit einem „Hello World“ durchführen.

 
 public static class MyUI extends UI {
    @Override
    protected void init(VaadinRequest request) {
      setContent(new Label("Hello World"));   // Attach to the UI
    }
  }

Zu guter Letzt fehlt nur noch, dass wir das neu geschriebene Servlet aktivieren. Hierzu müssen wir die Implementierung in der Methode main nur geringfügig abändern.

 
    DeploymentInfo servletBuilder
        = Servlets.deployment()
                  .setClassLoader(Main.class.getClassLoader())
                  .setContextPath(CONTEXT_PATH)
                  .setDeploymentName("ROOT.war")
                  .setDefaultEncoding("UTF-8")
                  .addServlets(
                      servlet(
                          MyProjectServlet.class.getSimpleName(),
                          MyProjectServlet.class).addMapping("/*")
                );

    DeploymentManager manager = Servlets
        .defaultContainer()
        .addDeployment(servletBuilder);

    manager.deploy();

    PathHandler path = Handlers.path(redirect(CONTEXT_PATH))
                               .addPrefixPath(CONTEXT_PATH, manager.start());

    Undertow undertowServer = Undertow.builder()
                                      .addHttpListener(8080, "0.0.0.0")
                                      .setHandler(path)
                                      .build();
    undertowServer.start();

Wir entfernen den HttpHandler und setzen stattdessen die Deployment-Informationen für das Servlet. Die notwendigen Angaben sind der Name des Servlets, die implementierende Klasse und das gewünschte Mapping. Hier kann man nun entscheiden, ob man das Mapping an dieser Stelle vornehmen möchte oder lieber die Annotation @WebServlet(..) verwendet. Ich persönlich bevorzuge die Definition mittel Deployment-Info, da wir dieses später für dynamische Aspekte nutzen können.

Der Entwicklungszyklus

In diesem Abschnitt wollen wir uns damit beschäftigen, wie wir das UI testen können. Ein Servlet, das eine reine Textnachricht zurücksendet, haben wir ja schon getestet. Damit haben wir zumindest schon einmal alles zusammen, um bei einem Test zu Beginn den Servlet-Conatiner zu starten, das gewünschte Servlet zu deployen und nach dem Test den Servlet-Conatiner wieder zu stoppen.

Hier werden wir uns nun ein wenig mehr Komfort erarbeiten. Als erstes beginnen wir mit der Main-Klasse, in der unser Undertow gestartet und gestoppt wird. Die Instanz des Undertow legen wir in eine Instanz der Klasse Optional, sodass wir in den Methoden start und shutdown darauf zugreifen können.

 
private static Optional<Undertow> undertowOptional;

Die beiden Methoden start und shutdown sind einfach und delegieren direkt auf die Instanz des Undertow durch.

 
  public static void start() {
    main(new String[0]);
  }

  public static void shutdown() {
    undertowOptional.ifPresent(Undertow::stop);
  }

Im jUnit-Test ergeben sich dadurch die nachfolgenden Änderungen.

 
  @Before
  public void setUp() throws Exception {
//    HelloWorldMain.main(new String[0]);
    HelloWorldMain.start();
  }

  @After
  public void tearDown() throws Exception {
    HelloWorldMain.shutdown(); 
  }

Da wir nun beginnen, mehrere Tests zu schreiben, werden wir dieses in eine Basisklasse mit dem Namen BaseTest auslagern. Zu Beginn werden wir Extraktionen durch Vererbung realisieren.

Bei dem ersten Test haben wir gesehen, wie wir eine Verbindung zu dem Servlet-Conatiner aufbauen, einen Request absetzen und das Ergebnis mit den Erwartungen abgleichen. Allerdings werden wir mit dem Ansatz in dieser Form nicht sehr weit kommen, wenn wir eine Web-App testen möchten, die komplexere Strukturen aufweist. Hierfür gibt es das Projekt Selenium, das uns genau hierbei helfen wird.

Selenium

Was genau ist nun Selenium? Selenium ist aufgeteilt in zwei Hauptbestandteile: zum einen Selenium WebDriver und zum anderen Selenium IDE. Wir werden uns hier mit Selenium WebDriver auseinandersetzen. Mit Hilfe von Selenium können wir einen Browser fernsteuern, damit unsere Anwendung aufrufen und die Ergebnisse verifizieren.

Praktisch bedeutet das nun Folgendes. Wir starten einen WebDriver, zum beispiel für Chrome. Damit starten wir lokal eine Instanz von Chrome, die wir dann programmatisch fernsteuern können. Nachfolgend rufen wir dann unsere lokal laufende Web-Anwendung auf und prüfen das Ergebnis bzw. suchen unseren Button, den wir anklicken möchten, etc.

Um die benötigten Abhängigkeiten zu erhalten, erweitern wir unsere pom.xml um folgende Einträge.

 
<properties>
    ...
    <selenium.version>3.4.0</selenium.version>
</properties>

<dependencies>
...
    <!--TDD Selenium-->
    <dependency>
      <groupId>org.seleniumhq.selenium</groupId>
      <artifactId>selenium-api</artifactId>
      <version>${selenium.version}</version>
      <scope>test</scope>
    </dependency>

    <dependency>
      <groupId>org.seleniumhq.selenium</groupId>
      <artifactId>selenium-java</artifactId>
      <version>${selenium.version}</version>
      <scope>test</scope>
    </dependency>

    <dependency>
      <groupId>org.seleniumhq.selenium</groupId>
      <artifactId>selenium-chrome-driver</artifactId>
      <version>${selenium.version}</version>
      <scope>test</scope>
    </dependency>
</dependencies>

Als erstes erzeugen wir eine Klasse mit dem Namen MyUITest, die von BaseTest erbt. Damit stellen wir sicher, dass unsere Anwendung bei jedem Test gestartet und gestoppt wird.

In unserem Test erzeugen wir eine Instanz eines WebDrivers, in unserem Fall die Implementierung ChromeDriver. Damit eine gewisse Zeit zwischen zwei Klicks vergeht, definieren wir noch ein WebDriverWait mit einer Verzögerung von zehn Sekunden. Damit können wir den Test ohne große Mühe selber mit dem Auge verfolgen.

Da wir eine Instanz der Klasse Webdriver haben, können wir unser Ziel, den URL http://localhost:8080/ mittels get(„http://localhost:8080/“) angeben. Zum Schluss, als letzte Aktion unseres Tests, schließen wir die allokierten Ressourcen, indem wir die Methode close() und quit() an unserem WebDriver aufrufen.

Nun sollte also Folgendes passieren. Der Test startet den Servlet-Container, deployt unser Servlet inklusive UI. Danach erzeugen wir einen WebDriver, der dann eine Instanz des Browsers Chrome starten und die URL http://localhost:8080/ aufrufen sollte. Danach soll alles wieder geschlossen und der Servlet-Conatiner gestoppt werden.

 
public class MyUITest extends  BaseTest {
  @Test
  public void test001() throws Exception {
    final WebDriver driver = new ChromeDriver();
    final WebDriverWait wait = new WebDriverWait(driver, 10);
    driver.get("http://127.0.0.1:8080/");
    driver.close();
    driver.quit();
  }
}

Leider passiert das nicht. Stattdessen erhalten wir beim Start des Tests eine Exception: IllegalStateException: The path to the driver executable must be set by the webdriver.chrome.driver system property;

Was vergessen worden ist, ist die Installation der WebDriver Binaries. Für Chrome findet man diese unter der hier.

Ich habe eine Version der Treiber dem Repository zur Kolumne auf GitHub unter _data/ beigelegt. Entpacken Sie das für das System entsprechende Archiv. Im Weiteren gehe ich davon aus, dass sich die Datei im Verzeichnis _data/ befindet.

Damit Selenium darauf zugreifen kann, muss man den Pfad zum Binary mit dem SystemProperty webdriver.chrome.driver zur Verfügung stellen. Das passiert nun als erstes im Test:

 
System.setProperty("webdriver.chrome.driver", "_data/chromedriver");

Wenn man nun den Test laufen lässt, passiert genau das, was wir erwarten.

Fazit

In diesem Teil unserer Serie haben wir den Servlet-Container Undertow ohne weitere Zusätze verwendet. Wir haben gesehen, wie er programmatisch initialisiert wird und wie wir unser Servlet deployen können. Damit haben wir nun eine Vaadin App, die im Vergleich zu der Lösung im vorherigen Teil sehr schnell gestartet und gestoppt werden kann.

Auf dieser Basis wurde eine Grundstruktur für UI-Tests mit Selenium geschaffen. Im nächsten Teil der Serie wollen wir uns damit beschäftigen, wie wir mittels Selenium lokal ausgeführte Browser-Tests für unsere Vaadin App schreiben können.

Den Quelltext unseres Beispiels findet sich auf GitHub. Bei Fragen und/oder Anregungen einfach melden unter sven[at]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

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.