Zwei WARs kommunizieren auf einem Tomcat – Code

07.04.2017

(Wow das Bild dieses Beitrages ist so schön, gibt es mehr davon? Ja, gibt es hier)

Im Artikel Zwei WARs kommunizieren auf einem Tomcat habe ich beschrieben, wie zwei WAR Dateien, also zwei Webapplikationen, auf einem Tomcat miteinander kommunizieren können. In diesem Beitrag mache ich ein konkretes, kleines Codebeispiel. Ich verwende dabei EclipseEE, welche speziell für Java EE Developers gemacht wurde.

Den Code zu diesem Artikel kann in folgendem ZIP heruntergeladen werden:
Klick mich für Code Download
MD5 Hash der Datei: 4de528a30d236dc2526f98da02c52fbf
(Kann etwa mit dem kostenlosen Tool WinMD5Free überprüft werden)

1. Erste Webapplikation erstellen

Als Erstes erstelle ich eine erste Webapplikation namens War1. Dazu gehe ich in Eclipse auf File -> New -> Dynamic Web Project und gebe den Projektnamen ein. Ein Klick auf Add project to working sets stellt sicher, dass das Projekt gleich in der Projektliste angezeigt wird. Mit Finish erstelle ich das Projekt.

Damit das Projekt im Browser angesprochen kann, mache ich als nächstes ein kleines Servlet. Dazwischen erstelle ich aber noch ein eigenes Package. Dazu rechtsklicke ich auf den src Folder und wähle New -> Package und gebe ihm den Namen war1package. Dann rechtsklicke ich auf das neue Paket und wähle New -> Servlet und gebe dem Servlet den Klassenname War1Servlet.

Falls Klassen wie HttpServletRequest nicht gefunden werden können halte ich die Maus über die Klasse und wähle im erscheinenden Dialog Fix Project setup und dann Add tomcat-embed-core-8.0.28 to build path of War1. Dies wird in Kapitel 4. Drei Maven Projekte machen mit einer Maven Dependency gefixt.

Erstellt man das Servlet auf diese Weise, wird automatisch eine doGet und doPost Methode inklusive dem HttpRequest und Response als Parameter angelegt. Im doGet erstelle ich nun für das Beispiel einen PrintWriter für eine einfache HTML Page.

War1Servlet doGet Methode:
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
   PrintWriter writer = response.getWriter();
   writer.println("<html>");
   writer.println("<head><title>Hello World Servlet 1</title></head>");
   writer.println("<body>");
   writer.println("<h1>Hello World from Servlet 1!</h1>");
   writer.println("</body>");
   writer.close();
}

Das Servlet kann jetzt bereits auf einen Tomcat geladen und gestartet werden. Im Tab Servers erstelle ich mit New -> Server einen Tomcat 8.0 Server. Im Dialog Add and Remove… lade ich das neue War1 auf den Server und starte diesen anschliessend.

Gehe ich nun im Browser auf http://localhost:8080/War1/War1Servlet sehe ich, wie mir das Hello World from Servlet 1! beinahe ins Gesicht springt. Die erste Beispiel-Webapplikation ist bereit.

2. Webapplikation erstellen

Nun erstelle ich eine zweite Webapplikation genau wie im vorherigen Kapitel, einfach mit War2 als Projekt und War2Servlet als Servletname. Im doGet kann ich den gleichen Code verwenden, um zu testen, ob es funktioniert – Bitte nicht vergessen, das Projekt dem Tomcat ebenfalls hinzuzufügen und den Tomcat neu zu starten.

Am Ende habe ich also zwei lauffähige Beispielapplikationen und will nun erreichen, dass War2 eine Methode SecretCode aus War1 aufrufen kann.

3. Shared Jar erstellen

Damit die zwei Applikationen miteinander sprechen können, müssen wir ein geteiltes Java Archiv erstellen, in welchem die Schnittstellen als Interfaces abgespeichert sind. Das hat den Vorteil, dass jede der beiden Parteien genau wissen, wie die Signaturen der einzelnen Methoden aussehen.

Ich mach also noch ein Projekt, dieses mal aber ein normales Java Projekt! New -> Java Project und nenne es SharedSecretCode.

Darin erstelle ich erstmal ein Interface namens SecretCode mit der Methode executeSecretCode. (Ich weiss, das ganze Projekt hat eigentlich nichts mit Security zu tun, mir gefällt einfach die Vorstellung, dass eine WAR Datei quasi den geheimen Code der zweiten WAR Datei ausführt.)

SecretCode.java:
public interface SecretCode {
   void executeSecretCode();
}

Was wir nun noch brauchen, ist ein eine InstanceHolder Klasse. Diese Klasse fungiert dabei als Singleton, es gibt sie also nur einmal beziehungsweise der InstanceHolder muss nicht instanziert werden. In diesem Holder erstellen wir ein Typ von dem eben erstellten SecretCode Interface inklusive Getter und Setter. Das Ziel ist es, dass War1 eine SecretCode Instanz erstellt, diese im InstanceHolder setzt und War2 dann auf diese Instanz mit dem Getter zugreifen kann.

Also auf das Package New -> Class und den InstanceHolder reingepappt. Merke: Da der Holder statisch ist, braucht es keine setInstance() Methode.

Holder.java:
public class Holder {

  // Klasse ist ein Singleton
  private static Holder instance = new Holder(); 
  private SecretCode service;

  public static Holder getInstance() {
    return instance;
  }
 
  public SecretCode getService() {
    return service;
  }

  public void setService(SecretCode service) {
    this.service = service;
  }
}

Am Besten richtet man sich den Projektaufbau so ein, dass man vom SharedSecretCode gleich eine richtige JAR Datei erstellt und diese in den Projekten War1 und War2 als externe JAR Datei importiert. Das kann ich mit der Hilfe von Maven machen.

4. Drei Maven Projekte machen

Aus den drei Projekten mache ich nun drei Maven Projekte. Dazu rechtsklicke ich auf ein Projekt und wähle configure -> convert to maven project. Eclipse wird mir dann in jedem Projekt ein pom.xml erstellen.

In jedem Projekt muss ich nun noch eine Configuration laufen lassen, damit das Projekt gebuilded wird. Der Prozess ist dabei bei jedem Projekt gleich: Ich rechtsklicke etwa das pom.xml vom Projekt SharedSecretCode und wähle Run As -> Maven Build… (mit den drei Punkten!) und fülle folgendes aus:

Name: SharedSecretCode clean install
Goals: clean install

Und diesen Build lasse ich dann mit Run laufen. Alle drei Projekte sollten von Maven auf ein BUILD SUCCESS kommen, damit alles stimmt.

Im Projekt SharedSecretCode wird dabei eine Datei SharedSecretCode-0.0.1-SNAPSHOT im Ordner target erstellt.

Im pom.xml von War1 und War2 füge ich noch die korrekte Version des tomcat-servlet-api ein, damit er Klassen wie HttpServletRequest findet. Maven braucht das JAR File ebenfalls als Maven Dependency, damit er zufrieden ist.

War1 und War2 pom File: 
<dependencies>
  <dependency>
    <artifactId>SharedSecretCode</artifactId>
    <groupId>SharedSecretCode</groupId>
    <version>${project.version}</version>
    <scope>system</scope>
    <systemPath>C:\eclipseWorkspaces\SpringEE\SharedSecretCode\target\SharedSecretCode-0.0.1-SNAPSHOT.jar</systemPath>
  </dependency>
  <dependency>
    <groupId>org.apache.tomcat</groupId>
    <artifactId>tomcat-servlet-api</artifactId>
    <version>8.0.43</version>
    <scope>provided</scope>
  </dependency>
</dependencies>

Nicht vergessen, die War1 und War2 ebenfalls mit clean install laufen zu lassen, damit die entsprechenden Pakete runtergeladen werden. Falls irgendwelche Fehlermeldungen kommen, ist es nie schlecht, einen Rechtsklick auf die Projekte zu machen und Maven -> Maven -> Update project laufen zu lassen.

5. Den SecretCode im War1 implementieren

Im War1 mache ich nun eine konkrete Implementation von dem Code, der von War2 ausgeführt werden soll. Ich nenne die Klasse SecretCodeImpl und implmentiere das vorher erstellte Interface SecretCode.

SecretCodeImpl.java:
public class SecretCodeImpl implements SecretCode {
  public void executeSecretCode() {
    System.out.println("Ich bin der supergeheime Code aus SecretCodeImpl");
  } 
}

Das Ziel ist also, dass War2 die Methode executeSecretCode ausführen kann. Sobald dies geschafft ist, ist es egal, was sich hinter dieser Methode verbirgt. Die Methode könnte Daten aus einer Datenbank lesen oder ein Programm ausführen, ganz egal. In diesem Beispiel mache ich einen einfachen println zur Demonstation.

6. SecretCodeImpl dem InstanceHolder übergeben.

Den aufzurufenden Code habe ich jetzt, nun erstelle ich im doGet von War1 eine Instanz von SecretCodeImpl und übergebe diese dem InstanceHolder, damit später War2 darauf zugreifen kann. Merke dass hier die Klassen Holder und SecretCode importiert werden müssen, damit diese bekannt sind.

Nötige Imports:
import mypackage.Holder;
import mypackage.SecretCode;

War1Servlet doGet Methode:
// Hier wird die Implementation dem Holder Singleton übergeben
SecretCode secretCode = new SecretCodeImpl();
Holder.getInstance().setService(secretCode);
System.out.println("Holder Instanz gesetzt");

7. Den geheimen Code im War2 aufrufen

Der letzte Schritt ist nun, den geheimen Code in War2 aus dem InstanceHolder zu holen und die entsprechende Methode aufzurufen. In der doGet Methode im War2 hole ich die Instanz und rufe die Methode auf. Auch bei War2 müssen die zwei Klassen importiert werden, damit diese bekannt sind.

Nötige Imports: 
import mypackage.Holder;
import mypackage.SecretCode;

War2Servlet doGet Methode:
SecretCode secretCode = Holder.getInstance().getService();
secretCode.executeSecretCode();

8. Es funktioniert nicht -> Shared Jar konfigurieren

Probiere ich jetzt, den Server zu starten, kriege ich eine Fehlermeldung:

Caused by: java.lang.NoClassDefFoundError: mypackage/SecretCode

Jetzt kommt der eigentliche Trick hinter dem ganzen Zwei WAR Dateien kommunizieren miteinander: Die zwei WAR Files haben zwar beide das Shared JAR als Abhängigkeit drin, es handelt sich aber jeweils um eine eigene Kopie – Beziehungsweise der Classloader jeder Webapplikation lädt sein eigenes JAR. Siehe auch die Theorie im Artikel Zwei WARs kommunizieren auf einem Tomcat

Ich sage den zwei WAR Dateien nun, dass sie dasselbe JAR verwenden sollen. Sie sollen beide das JAR vom Shared ClassLoader erhalten beziehungsweise auf das gleiche JAR zugreifen.

Dazu muss ich den Shared Loader Eintrag in der Datei Catalina.Properties um die Class Dateien des Shared JAR ergänzen.

catalina.properties ist normalerweise im Installationsordner von Tomcat – Meistens gibt es aber eine eigene Kopie. Der einfachste Weg, um den Pfad zu finden, ist es, den Server zu starten und nach dem Eintrag CATALINA_BASE Ausschau zu halten.

INFORMATION: CATALINA_BASE: C:\eclipseWorkspaces\SpringEE\.metadata\.plugins\org.eclipse.wst.server.core\tmp0

In diesem Ordner hat es einen Unterordner Conf und darin findet sich die catalina.properties Datei.

Darin ergänze ich den Eintrag Shared Loader um den Pfad zu SharedSecretCode -> Target -> Classes.

shared.loader=C:/eclipseWorkspaces/SpringEE/SharedSecretCode/target/classes

Merke dass man Schrägstriche verwenden muss und nicht Backslashes wie in Windows Explorer.

9. Fertig -> Geheimen Code aufrufen

Ist alles kompiliert und der Tomcat neu gestartet, kann es also losgehen. In diesem Beispiel muss ich zuerst http://localhost:8080/War1/War1Servlet aufrufen, damit der InstanceHolder gesetzt wird (sonst gibt es eine NullPointerException). In einem echten Projekt würde man diesen Code etwa beim Start des Tomcats ausführen lassen.

In der Konsole sehe ich nach dem Aufruf von Web1Servlet:

Holder Instanz gesetzt

Wenn ich im Browser dann http://localhost:8080/War2/War2Servlet aufrufe, sollte aus War2 der geheime Code von War1 ausgeführt werden.

Und tatsächlich sieht man in der Konsole den println:

Ich bin der supergeheime Code aus SecretCodeImpl

War2Servlet hat also den Code aus War1Servlet über das SharedJar hinaus ausgeführt.

Zwei WARs kommunizieren auf einem Tomcat

07.04.2017

Eine Schritt-für-Schritt Anleitung, wie man dieses Vorhaben konkret in Code umsetzt, finden Sie in meinem Artikel Zwei WARs kommunizieren auf einem Tomcat – Code

WAR steht bekanntlich für Web application ARchive und beinhaltet eine Webapplikation. Mehrere Webapplikationen können ganz einfach auf einem Tomcat deployed werden, indem sie in den Ordner Webapps vom Tomcat Server kopiert werden.

Nun ist es aber auch möglich, dass diese zwei Applikationen miteinander kommunizieren – Und ganz konkret meine ich damit: Dass zwei WAR Dateien gegenseitig ihre Java Methoden aufrufen und so Informationen austauschen oder Operationen anstossen können.

Zuerst einmal braucht es ein Shared Jar

Damit die beiden Applikationen miteinander reden können, braucht es zuerst eine JAR Datei mit den Schnittstellen. Konkret ist dies ein Java Projekt, welches public interfaces beinhaltet und welches am Ende in einer JAR Datei endet. Dieses muss dann auch auf den Tomcat deployed werden.

Diese Interfaces beinhalten die Methodenköpfe (Signaturen), welche von einem der beiden Webapplikationen implementiert werden müssen. Mithilfe dieser JAR Datei ist aber beiden Webapplikationen klar, wie die Schnittstellen zwischen ihnen aussehen und welche Parameter sie übergeben können.

Im Shared Projekt sind dann also die Interfaces, in einer der beiden Webapplikationen die effektive Implementation dieser Interfaces.

Das Geheimnis: Shared Class Loader

Wie ich in meinem Artikel Was ist ein class path/class loader in Tomcat? geschrieben habe, werden die Ressourcen von verschiedenen class loaders beim Starten von Tomcat geladen.

Wenn zwei Webapplikationen dieselbe JAR Datei verwenden, ist das normalerweise kein Problem. Beide Applikationen bekommen einfach eine Kopie davon und können unabhängig voneinander auf diese zugreifen. In unserem Fall wollen wir aber etwas anderes: Die JAR Datei soll quasi die Brücke zwischen den beiden WAR Dateien sein.

Damit Tomcat eine JAR Datei als externes Repository ladet, muss diese in den catalina.properties beim shared loader im richtigen Format angegeben werden:

shared.loader=file:/pfad/zur/datei/meinJar.jar

Deveveloper Tipp 1: Während man aktiv an der Applikation arbeitet, kann man alternativ auch einfach den Klassenordner zu den Klassen angeben, die am Schluss im Shared Jar verwendet werden:

shared.loader=C:/MyApplication/MyAppShared/target/classes

Und wie funktioniert das nochmals?

Ganz einfach: Man hat zwei WAR Webapplikationen, die man auf einen (!) Tomcat kopiert. Die kennen sich aber normalerweise nicht. Da wir aber nun noch eine JAR Datei mit den Schnittstellen laden, und dieses dem Tomcat shared beziehungsweise parent class loader mitteilen, kennen beide WAR Files diese Interfaces. Eine Webapplikation kann diese Interfaces nun implementieren und die andere Webapplikation kann diese aufrufen, womit die beiden Applikationen miteinander kommunizieren können.

Deveveloper Tipp 2: Wenn man an einer Webapplikation arbeitet, und die andere nicht zur Verfügung hat, kann man in einem Testprojekt selber die Implementation eines Interfaces vornehmen. So kann man zum Beispiel eine Methode einbauen, bei der die andere Webapplikation eigentlich eine Liste von Namen aus einer Datenbank lesen müsste – stattdessen gibt man für Testzwecke einfach hartcodiert eine Liste von ein paar Namen zurück.

Die zwei WAR Dateien müssen die JAR Datei als Dependency in der pom.xml Datei angeben. Das wiederum heisst, dass man am Besten Maven Projekte erstellt.

Gerne weise ich hier nochmals auf meinen Artikel Zwei WARs kommunizieren auf einem Tomcat – Code hin, der das Ganze als Codebeispiel zeigt und nach dessen Lektüre alles viel klarer sein sollte.

Was ist ein Load Balancer?

Nehmen wir an, Sie hätten eine Java Webapplikation in Spring programmiert. Eine hochexklusive Homepage, welche dem Besucher die Frisuren aller Formel 1 Rennfahrer zeigt. Nun möchten Sie ihr Glanzstück dem Internet zeigen.

Im einfachsten Fall erstellen Sie ein aus ihrem Java Projekt eine WAR Datei (Web Archive). Dann brauchen Sie noch einen Server, der zum Beispiel eine Linux Distribution am laufen hat. Dort installieren sie einen Tomcat (siehe Tomcat oder Websphere?) und kopieren ihre WAR Datei in den Webapps Ordner. Beim Starten von Tomcat wird ihre Webapplikation automatisch entpackt und gestartet.

Unerhoffter Erfolg

Nach einigen Wochen schauen Sie sich die Benutzerstatistiken an und sehen, wie die Zugriffe auf ihre Seite explodiert sind! Alle wollen Formel 1 Rennfahrerfrisuren bestaunen! Ihr Server kommt mit den Anfragen nicht mehr nach. Was tun?

Mehr Server müssen her

Sie brauchen mehr Server und Sie brauchen ein Load Balancing!

Sobald Sie mehrere Server am Laufen haben spricht man von einem Server Cluster (oder auch Server Farm oder Server Pool). Ein Cluster ist eine Gruppe von Servern, die gleichzeitig eine Webapplikationen laufen haben und für die Aussenwelt so erscheinen, als ob es ein einziger Server wäre.

Zwischen der Aussenwelt und den Servern liegt dann der Load Balancer. Dies kann eine Software- oder Hardwarelösung sein.

Das Ziel ist es, die Hochverfügbarkeit zu gewährleisten. Wenn ein Server ausfallen würde, können die verbleibenden Server die Anfragen übernehmen. Ein weiteres Ziel ist die Skalierbarkeit. Wenn ihre Benutzerzahlen in den Himmel schiessen, werden Sie noch mehr Server benötigen, um den Ansturm an Besuchern auszuhalten. Das System muss also skalierbar und erweiterbar sein.

Wie funktioniert jetzt ein Load Balancer?

Wenn ein Besucher auf ihre Webseite geht, tippt er eine URL wie „www.suesseformel1frisuren.ch“ in den Browser. Der Domain Name Server (DNS) schaut dann in seinem Verzeichnis und findet die IP Adresse des Servers, etwa „200.0.0.121“, welche er dem Besucher zurückgibt. Das nennt man DNS Lookup.

Der DNS Server kann nun mehrere Adressen für eine URL speichern. Jede Adresse steht dann für einen Server im Cluster. Kommt nun eine Anfrage rein, schickt der DNS Server die Adresse des ersten Servers zurück. Bei der nächsten Anfrage die des zweiten Servers, dann des dritten, dann wieder der erste und so weiter. Das ist die Round Robin-Methode.

200.0.0.121
200.0.0.122
200.0.0.123

Was ist der Vorteil dieser Methode?

Der Vorteil davon ist, dass neue Server ganz einfach der Konfiguration hinzugefügt werden können.

Was ist der Nachteil bei der Server Affinität?

Server Affinität ist die Fähigkeit, die Benutzeranfragen an einen bestimmten Server zu senden. Ein Nachteil dieser Methode ist es nämlich, dass mehrere Anfrage des gleichen Benutzers an verschiedene Server geschickt werden können. Das HTTP Protokoll ist stateless, zwei aufeinanderfolgende Anfragen wissen nichts voneinander. Um die Kontrolle über die Session eines Benutzers zu behalten, kann der Server eine der folgenden Methoden verwenden:

1. Cookies
2. Versteckte Felder
3. URL Rewriting

Das Prinzip ist dabei jeweils dasselbe: Wenn ein Benutzer eine erste Anfrage macht, erstellt der Server ein Token, welches diesen Benutzer identifiziert. Dieses Token wird dann an den Benutzer geliefert, und dieser schickt immer das gleiche Token bei folgenden Anfragen. Somit kann der Load Balancer die Anfragen des gleichen Tokens immer an den gleichen Server schicken.

Ein Problem hierbei ist allerdings, dass der Browser die IP Adresse des Servers cached. Wenn dieser Cache abläuft, macht der Browser eine neue Anfrage und erhält gegebenfalls eine andere IP als vorher.

Was ist der Nachteil bei der hohen Verfügbarkeit?

Hat man einen Cluster mit 5 Servern und einer dieser Server geht offline, kann es dennoch vorkommen, dass Anfragen an diesen Server geschickt werden. Um dies zu verhindern prüfen moderne Router regelmässig, ob die Server noch angesprochen werden können.

Gibt es noch andere Load Balancing Algorithmen?

Ja, statt eines Round Robins könnte man etwa wählen, dass jeweils derjenige Server die nächste Anfrage erhält, welcher momentan am wenigsten offene Verbindungen zu Clients hat.

Oder man konfiguriert ein IP Hashing, bei dem man anhand der IP Adresse des Benutzers feststellt, an welchen Server er weitergeleitet wird.

Hardware und Software Load Balancers

Bei Load Balancers gibt es Lösungen als Hardware wie auch als Software.

Ein Hardware Load Balancer hat etwa den Vorteil, dass er selber eine virtuelle Ip Adresse besitzt, die er der Aussenwelt zeigen kann. Diese virtuelle Ip repräsentiert quasi den gesamten Cluster. Kommt nun eine neue Anfrage rein, überschreibt der Load Balancer den Request Header mit der effektiven Adresse eines Servers.

Der Vorteil ist hier, dass man Server einfach entfernen kann und nicht Gefahr läuft, dass dennoch ein Benutzer auf diesen Server connected – Schliesslich wird der gesamte Cluster durch die eine virtuelle IP repräsentiert. Das Problem ist, dass Hardware Load Balancer nicht in gesicherten HTTPS Verbindungen laufen, weil dort die Nachrichten SSL-verschlüselt sind und den Load Balancer daran hindern, auf die Informationen zuzugreifen. Die Server Affinität ist somit bei HTTPS nicht gegeben. Für die Entschlüsslung müsste ein Web Server Proxy vor den Cluster gehängt werden.

Software Load Balancer werden auch Application Delivery Controller (ADC) genannt, welche gut mit Virtual Appliances umgehen können – Also etwa ein Betriebssystem, Tomcat und Webapplikation als geschnürtes Paket.

Momentan mal! Virtual Applicances? Huh?

Genau. Am Anfang habe ich das einfachste Beispiel gemacht: Sie kaufen einen physischen Server, stellen ihn in einen Raum und installieren zum Beispiel eine Linux Distribution mit Tomcat auf dieser Maschine. Dort läuft dann ihre Webapplikation.

In den heutigen Firmen ist dieses einfache Beispiel aber mittlerweile ziemlich entfernt von der Realität. Stellen Sie sich vor, eine Firma müsste für jede Webapplikation eine bis mehrere physische Server unterhalten. Das wäre ein viel zu grosser Aufwand.

Stattdessen läuft heute mittlerweile so ziemlich alles virtualisiert – So virtualisiert, wie man sich das kaum vorstellen kann. Eine grosse Firma hat etwa eine gewisse Serverlandschaft zur Verfügung. Auf dieser Serverlandschaft laufen alle ihre Webapplikationen – Aber alle stets virtualisiert. Ihre Formel 1 Frisuren-Applikation ist dann etwa nur ein virtuelles Image, in dem schon alles enthalten ist: Applikation, Betriebssystem und Tomcat. So kann man die Applikation auf beliebig vielen Servern starten und wieder verschwinden lassen, ganz wie man es benötigt (Punkt Skalierbarkeit).

Liefert man eine neue Version seiner Applikation, macht das Entwicklungsteam oftmals gleich das Gesamtpaket (also das ganze Image) für den Betrieb bereit, welcher dieser nur noch deployen kann. Entgegen meinem Artikel Was ist DevOps? ist die Entwicklung und der Betrieb oftmals noch in verschiedenen Teams. Die Entwickler entwickeln, die Betriebsleute installieren.

Nun, es geht sogar noch weiter, aber ich muss wohl langsam zum Ende kommen. Weitere interessante Themen sind etwa Docker, bei dem man die Applikation auch noch vom Betriebssystem abkoppelt, oder serverless code, bei dem alles irgendwo in der Cloud läuft. Aber diese Geschichte erzähle ich ein anderes Mal.