Suche
Teil 21: Backend meets Frontend: Trainer for kids 11

Änderungen am UI: Das muss noch schneller gehen

Sven Ruppert

© Shutterstock / HelenField

Eine Webanwendung für jede noch so kleine oder große Änderung komplett neu zu starten, frisst unglaublich viel Zeit. Schneller wird die Entwicklungs- und Testphase, wenn wir es möglichen machen, dass die Anwendung veränderte Klassen zur Laufzeit erkennt.

Im letzten Teil der Serie haben wir den Start der JVM beschleunigt. Ziel war es, den Startup für einen einzigen Test so schnell wie möglich ablaufen zu lassen, um so viel Zeit wie möglich für die Entwicklung selbst nutzen zu können. Erreicht haben wir dieses mit den folgenden Parametern.

-Xmx128m 
-XX:+TieredCompilation 
-XX:TieredStopAtLevel=1 
-Xverify:none

Das ist aber noch lange nicht alles, was wir erreichen können. Gut wäre es, wenn wir den Start der Anwendung komplett vermeiden könnten. Damit kommen wir in den Bereich, in dem nur die veränderten Anteile einer Anwendung erneut geladen werden sollen; und das natürlich zur Laufzeit. Hierfür brauchen wir derzeit noch Werkzeugunterstützung.

Zusätzlich zu den Quelltextbeispielen zu diesem Artikel verwende ich auch die Sourcen des Open-Source -Projekts Functional-Reactive. Die Sourcen befinden sich auf GitHub. Ebenfalls werde ich damit beginnen, funktionale Aspekte in die Entwicklung einfließen zu lassen. Hierzu stütze ich mich auf die Serie hier auf JAXenter unter dem Namen Checkpoint Java.

Die kommerzielle Variante

Ein kommerzieller Vertreter, der hier helfen kann, ist aus dem Hause Zeroturnaround: jRebel. Hierbei handelt es sich um einen Classloader, der es ermöglicht, dem System neue und veränderte Klassen zur Laufzeit zur Verfügung zu stellen. Um die Software ausprobieren zu können, gibt es zwei verschiedene Wege. Der Erste ist es, die Probeversion zu nutzen. Hierzu kann man auf der Webseite einige personenbezogene Daten eingeben und erhält dafür eine 14-Tage-Testversion. Der zweite Weg geht über die Registrierung. Unter dieser Webaddresse ist eine kostenlose Version erhältlich, die für die Entwicklung von Open Source Software verwendet werden darf. Zu beachten ist, dass zusätzlich erfordert ist, dass jRebel die Verwendungsstatistiken, die unter dieser Lizenznummer laufen, auf einem Facebook- oder Twitter-Account wöchentlich veröffentlichen darf.

Kommen wir aber nun zu den technischen Details. Um mit dem Werkzeug an der eigenen Anwendung arbeiten zu können, muss beim Start ein Agent angegeben sein. Dieser Agenten bildet das Nachladen der Klassen ab. Dank der guten Unterstützung aller gängigen IDEs können wir hier auf eine grafische Handhabung zurückgreifen. Hierzu einfach das angebotene Plug-in für die jeweils verwendete IDE auswählen und installieren. Natürlich kann das auch alles auf der Kommandozeile erledigt werden. Hierfür verweise ich auf die Doku von Zeroturnaround. Nachdem das Plug-in installiert und die Lizenz aktiviert worden ist, kann die eigene Anwendung mit aktivem jRebel gestartet werden. Bei IntelliJ kann man hierfür einfach die angebotene Startkonfiguration verwenden. Das Vorgehen unterscheidet sich bei den einzelnen IDEs geringfügig.

Nachdem der Server gestart worden ist, kann mit der Modifikation begonnen werden. Wenn eine Klasse bearbeitet worden ist, wird diese neu kompiliert. jRebel erkennt diese und beginnt automatisch damit die Klasse erneut zu laden und dem Classloader als aktuelle Version dieser Klasse zur Verfügung zu stellen. Nun stellt sich die Frage, wie der Entwickler die Veränderung auch sehen kann. Bei Webanwendungen lässt sich das meist mit dem erneuten Laden der Webapplikation provozieren. Das bedeutet allerdings auch, das die Instanz, die zu dem Ergebnis geführt hat, nicht vom Webserver oder Servlet-Container behalten wurde. Hier zahlt es sich erneut aus, wenn mit zustandslosen Klasseninstanzen gearbeitet wird, die bei der Abarbeitung der Aufgabe erneut erzeugt werden. Das ist natürlich nicht überall sinnvoll. Deshalb ist es unser Ziel, die Möglichkeit zu schaffen, dass der Entwickler das erneute Erzeugen der Instanz so einfach wie möglich provozieren kann. Wir lösen es so, dass die Instanz der Komponente, die zum Test dargestellt wird, jedes mal neu erzeugt wird, wenn der Button refresh gedrückt wird.

Die Open-Source-Variante

Es gibt auch eine kostenlose Alternative zu jRebel: Die Open-Source-Software mit Namen DCEVM, ein Akronym für Dynamic Code Evolution Virtuell Machine. Das Projekt kann man auf GitHub finden und basiert auf der Doktorarbeit von Dr. Thomas Würthinger, der mittlerweile als Graal Project Lead bei Oracle arbeitet. Derzeit unterstützt werden die JDK-Versionen 7 und 8 (bis Build 144). Allerdings konnte ich keine Information darüber finden, ab wann das JDK 9 unterstützt wird. Wem also JDK 8 in der Entwicklung ausreicht, der kann sofort loslegen.

Bevor wir mit der Entwicklung beginnen können, sind ein paar Vorbereitungsschritte notwendig. Das Projekt HotSwapAgent hilft mit einer grafischen Oberfläche, um DCEVM als Alternative VM zu registrieren (Abb. 1). Mit java -jar DCEVM-8u144-installer.jar kann man die grafische Oberfläche starten, um danach das JDK auszusuchen, das modifiziert werden soll. Ich persönlich habe das JDK einfach ein zweites Mal parallel zum Bestehenden installiert und verwende dieses für die Arbeit mit DCEVM.

Abb. 1: Die Vorbereitungen für DCEVM

Wenn die Vorbereitungen abgeschlossen sind, kann die Anwendung mit -XXaltjvm=dcevm -javaagent:hotswap-agent.jar im Debugger gestartet werden. Die Verwendung ist genau die gleiche wie bei jRebel. Es werden eine oder mehrere Klassen modifiziert und danach kompiliert. Die Veränderung wird von dem System erkannt und aktiviert. Das wird durch das erneute Erzeugen der Komponente sichtbar. Die Verarbeitungsgeschwindigkeit ist gefühlt bei beiden Produkten gleich schnell. Was kann aber alles modifiziert werden? Um das herauszufinden, werden wir eine Beispielkomponente erzeugen und anhand dieser einige Tests durchführen.

Wer kann mehr?

Als Testklasse soll die bisher noch nicht verwendete Klasse ReportComponent zum Einsatz kommen. An ihr führen wir mehrere Veränderungen durch. Es werden bestehende Komponenten verändert, zwei Buttons hinzugefügt, inklusive dem ClickListener, und ein neuer Service wird via @Inject bereitgestellt. Das lässt sich mit unseren beiden Produktvarianten problemlos realisieren. Um den Vorgang selbst ein wenig angenehmer zu gestallten, erweitern wir die Anwendung zur Darstellung der Komponente ein wenig. Ziel ist es, das der Entwickler den Quelltext modifiziert und dann ausschließlich kompilieren muss. Die Anwendung selbst soll dann eine neue Instanz der Komponente erzeugen und zur Darstellung bringen. Um dieses Verhalten zu erreichen, wird ein Timer gestartet, der die Komponente in regelmäßigen Abständen erneut erzeugt und damit die alte Instanz ersetzt.

  //on class level
  private Result<UI> ui     = ofNullable(getUI(), "ui not init until now");
  private final Timer timer = new Timer(false);
  
  //inside method postConstruct 
      timer.scheduleAtFixedRate(new TimerTask() {
      @Override
      public void run() {
        Logger.getLogger(DevComponent.class).info("Timer Task is running..");
        ui.ifPresentOrElse(
            yes -> yes.access(() -> {
              if (autoRefreshComponent.getValue()) {
                classComboBox
                    .getSelectedItem()
                    .ifPresent(refreshFkt()::accept);
              }
            }),
            no -> Logger.getLogger(DevComponent.class).info("UI not present..")
        );
      }
    }, REFRESH_RATE_MS, REFRESH_RATE_MS);

Das Setzen der Komponente selbst muss im UI-Thread passieren. Das wiederum kann erreicht werden, indem die aktuelle Instanz der UI geholt und ihr dort mittels der Methode access(..) ein Runnable übergeben wird, in dem die Modifikationen und das Auslösen des Push-Events ausgeführt werden.

  private Consumer<Class<?>> refreshFkt() {
    return (clazz) -> ui
        .ifPresentOrElse(
            uiPresent -> switchFkt().andThen(uiPresent::access).apply(clazz),
            uiMissing -> Logger.getLogger(DevComponent.class).info("UI not present..")
        );
  }

  private Function<Class, Runnable> switchFkt() {
    return (clazz) ->
        () -> ((CheckedFunction<Class<?>, Component>)
            aClass -> (Component) activateDI(aClass))
            .apply(clazz)
            .ifPresentOrElse(
                success -> {
                  success.setSizeFull();
                  testComponentPanel.setContent(success);
                  ui.get().push();
                },
                Logger.getLogger(clazz)::warning
            );
  }

Wie das dann praktisch aussehen kann zeigt das nachfolgende Video:

Was möglich ist

Alle Möglichkeiten zu zeigen, ist leider nicht möglich. Aber es gibt ein paar Dinge, die recht kompliziert abzubilden sind und deswegen von den Produkten eher selten oder gar nicht realisiert worden sind. Kommen wir zu den Dingen, die beide Produkte ermöglichen.

  • Hinzufügen/entfernen/modifizieren von Klassenfeldern
  • Hinzufügen/entfernen/modifizieren von Methoden
  • Hinzufügen/entfernen/modifizieren von Methoden-Annotationen
  • Hinzufügen/entfernen/modifizieren von Klassen, inklusive anonymer Klassen
  • Neudefinitionen von anonymen Klassen korrigieren
  • Hinzufügen/entfernen von statischen Membern von Klassen
  • Handhabt die statische Member-Initialisierung
  • Hinzufügen/entfernen von Enum-Werten
  • Aktualisieren der Framework- und Application-Server-Einstellungen

Das Zusammenspiel mit DI-Frameworks ist eher etwas, das durch den Entwickler selbst realisiert werden kann und auch sollte. Je nachdem welches Framework verwendet wird, muss ein erneutes Scannen der verfügbaren Klassen und Interfaces durchgeführt werden. Das lässt sich leicht durch einen Button auslösen, der die notwendigen Reinitialisierungen anstößt. Natürlich kann das auch bei jedem Reload automatisch erfolgen. Ob das zu Verzögerungen führt, die dann doch wieder stören, ist von der Größe des Projektes und dem jeweiligen DI-Framework abhängig. Damit können komplett neue Klassenstrukturen geschaffen und auch gleich verwendet werden.

Einen Unterschied gibt es allerdings zwischen den beiden Produkten. DCEVM ist in der Lage, der Vererbung etwas hinzuzufügen, in unserem Beispiel, dass eine Klasse ein Interface implementieren kann. Wird das jedoch wieder entfernt, bekommt der Benutzer eine Fehlermeldung angezeigt, das diese Operation nicht unterstützt ist.

Anders ist das bei jRebel. Hier werden auch Veränderungen in der Vererbungshierarchie aktiviert.

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

Wenn man sich beide Produkte genauer ansieht, kann folgendes zusammentragen:

Bei DCEVM:

  • keine Lizenzen, die ablaufen können
  • keine Verbindung aus dem eigenen Netzwerk heraus notwendig
  • Basis für den JEP 159
  • keine Reduktion in der Vererbung möglich, Neustart erforderlich

Bei JRebel:

  • kommerzielles Produkt
  • Lizenzkosten pro Entwickler
  • regelmäßige Verbindung zum Internet notwendig
  • Im Gegensatz zu DCEVM Reduktion in der Vererbung möglich

Hierbei handelt es sich nicht um eine 100-Prozent-Auflistung aller Unterschiede. Allerdings sollten diese Kernpunkte dabei helfen, eine erste Endscheidung zu treffen, welches Tool man ausprobieren sollte. Ich kann es jedem Entwickler nur empfehlen, sich diese Werkzeuge einmal anzusehen und praktisch damit zu arbeiten.

Den Quelltext 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

Schreibe einen Kommentar

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