Erhältlich ab: Juni 2016
Erfolg im Web ist ein zweischneidiges Schwert. Gehen Besucherzahlen plötzlich durch die Decke, sinkt die Performance in den Keller. Leider lässt sich aber nicht jedes System flexibel skalieren, um den wechselnden Anforderungen Rechnung zu tragen. In diesem Artikel präsentieren wir, was Skalierbarkeit für Architektur bedeutet und auf welche Punkte man achten sollte.
Es gibt viele Gründe für die plötzliche Popularität einer Website. Der bekannteste ist wohl der so genannte Slashdot-Effekt. Slashdot war mal ein sehr beliebter Newsservice mit hohen Besucherzahlen, der heute aber an Bedeutung verloren hat. Erschien damals ein Artikel mit einem Link auf eine weniger bekannte Seite, so verspürte diese einen rasanten Anstieg im Traffic. Sie wurde „geslashdottet“, und oft endete dies mit einem totalen technischen Versagen, weil die Kapazität nicht ausreichte. Genau hier kommt die Skalierbarkeit (Abb. 1) ins Spiel: Sie bestimmt, zu welchem Grad sich die Kapazität eines Systems anpassen lässt. Im Gegensatz zu einem DOS-Angriff, ist ein Abschuss durch Slashdotting ungewollt.
Dank moderner Cloud-Infrastruktur haben wir heute Mittel und Wege, um dem Slashdot-Effekt wirkungsvoll begegnen zu können. Allerdings müssen wir hierfür einiges tun, denn geschenkt bekommt man die Skalierbarkeit leider nicht. Grundsätzlich unterscheidet man zwischen vertikaler und horizontaler Skalierbarkeit. Horizontale Skalierbarkeit (scaling out) bedeutet, dass mehr logische Einheiten verfügbar gemacht werden, z. B. durch das Aufschalten neuer Server. Vertikale Skalierung (scaling up) meint das Hinzufügen von mehr Ressourcen zu einer logischen Einheit, beispielsweise durch die Bereitstellung von zusätzlichem Hauptspeicher oder eine zusätzliche CPU. Hierfür musste man früher neue Mainboards installieren, heute ist es eine Konfigurationseinstellung im Hypervisor. In Cloud-Infrastrukturen heißt horizontale Skalierbarkeit Elastizität [1].
Die Elastizität ist zusammen mit der Resilience eine der beiden Hauptmerkmale einer Cloud-native-Applikation. Damit eine Anwendung also tatsächlich von den Vorzügen einer Cloud-Infrastruktur profitieren kann, muss sie vollständig horizontal skalierbar sein. Es reicht also nicht, eine bestehende Anwendung auf einer VM zu installieren. Das Anwendungsdesign und die Systemarchitektur müssen für Cloud-native ausgelegt sein. Bei bestehenden, proprietären Systemen kommt außerdem das verwendete Lizenzmodell hinzu. Oft lassen sich diese Anwendungen deswegen gar nicht dynamisch skalieren, sodass Workarounds wie das Bridgehead-Pattern notwendig werden (mehr im Abschnitt „Dynamik dank Bridgehead-Pattern“).
In der Regel lässt sich auch nur eine Cloud-native-Anwendung dynamisch skalieren. Bei einer dynamischen Skalierung kann im laufenden Betrieb horizontal skaliert werden, das heißt, es werden neue Instanzen hinzugefügt. Vertikale Skalierung ist in der Cloud zwar theoretisch ebenfalls dynamisch denkbar, aber das kann nicht mal Amazon EC2. Hier muss bei der Anpassung der Instanzgröße die Maschine neu gestartet werden.
Eine moderne, weltweit verteile Webanwendung besteht aus dem Frontend und dem Content Delivery Network (CDN), skizziert in Abbildung 2. Das Frontend lebt auf dem Browser und skaliert unkontrolliert über die Menge der Benutzer, die das System verwenden. Das CDN skaliert durch den betreibenden Provider automatisch und hält unsere Assets lokal beim Benutzer vor. Hier haben wir nur durch die Auswahl des richtigen Providers die Möglichkeit der Skalierung. Das Backend und die Integrationsschicht sind durch ihre Architektur durch uns kontrollierbar, hier können wir dann auch flexibel skalieren, solange die Architektur das zulässt. In beiden Schichten kommen unterschiedliche Technologien zum Einsatz. Im Rahmen des Trends hin zu Microservices verlagern sich die Geschäfts- und Datendienste stetig auf die Integrationsschicht, sodass die Backend-Schicht immer leichtgewichtiger wird. Auf der anderen Seite finden wir im Backend die schwergewichtigen, proprietären Monolithen großer CMS- und E-Commerce-Systeme, die sich schon aufgrund ihrer Lizenzmodelle nicht dynamisch skalieren lassen. Hinter der Integrationsschicht finden wir in Abbildung 2 zusätzliche zu integrierende Dienste, die wir nicht selbst kontrollieren können.
In Bezug auf die Skalierung ist so eine Schichtenarchitektur heikel, weil zum einen pro Schicht eine eigene Skalierungsstrategie benötigt wird, und zum anderen die Skalierung einer Schicht von der Skalierbarkeit der darunter liegenden Schicht abhängig ist. Angenommen, man hat für eine linear skalierende Integrationsschicht gesorgt, aber diese hängt leider von einem Service ab, der sich nicht oder nur schwierig skalieren lässt. Solch einen Service nennt man dann einen Bottleneck. Bottlenecks sollten im Vorfeld in der Architektur erkannt und möglichst gut aufgelöst werden.
Das skalierbarste Design heißt shared nothing. Bei diesem Design ist jede Instanz völlig unabhängig von allen anderen Systemteilen und kann dadurch linear skalierbare Kapazität erreichen – fantastisch, aber das muss man erstmal schaffen. Das bedeutet unter anderem, das gesamte Messaging asynchron machen zu müssen.
Eine Grundbedingung, um eine Schicht unserer Applikation horizontal skalieren zu können, sind die gespeicherten Informationen (State), die diese Schicht enthält. Betrachten wir als Beispiel einen einfachen E-Commerce Store. Das HTTPS-Protokoll, auf dem wir unser Backend erreichen können, ist stateless. HTTPS speichert keine Informationen, die einen Benutzer identifizieren. Um die eindeutige Identifikation von einem Kunden zu ermöglichen, verwenden wir einen Cookie. Der Cookie beinhaltet eine Identifikationsnummer, die uns ermöglicht, Aktionen des Kunden festzuhalten und personalisierte Informationen darzustellen. Unser Backend muss z. B. den Warenkorb des Kunden speichern. Wo wir diesen Warenkorb abspeichern, wird die Skalierbarkeit unseres Backends beeinflussen.
Die beste Performance erreicht das Backend, wenn der Warenkorb direkt im Memory vom System abgelegt wird. Wenn wir aber horizontal skalieren möchten, dann sprechen wir mit potenziell dutzenden Systemen und müssen aber den Warenkorb von einer Person ausfindig machen können. Man spricht in diesem Fall von „sticky sessions“: Anfragen von einer Session müssen immer auf das gleiche System geroutet werden. Mit einem Load Balancer kann man dieses Routing sicherstellen, aber man verliert die Möglichkeit, die Systeme beliebig zu skalieren, da ein Benutzer immer mit dem gleichen System kommunizieren muss. Wenn wir also horizontal skalieren und neue Systeme zu unserem Backend hinzufügen, können wir den bestehenden Traffic nicht einfach neu verteilen, sondern nur neue Sessions verteilen. Wenn wir ein System wieder entfernen, müssen wir erst die Sessions auslaufen lassen, damit der Kunde nicht während des Einkaufs plötzlich einen leeren Warenkorb hat.
E-Commerce-Systeme bieten normalerweise die Möglichkeit, eine Session extern zu speichern und somit den State zu verlagern und stateless zu werden. Je nach Anspruch an die Performance kann die Session in eine Datenbank, ein Dateisystem oder eine Caching-Lösung geschrieben werden. Die Session muss bei jedem Aufruf abgefragt werden. Es eignet sich, eine In-Memory-Lösung wie Memcached oder Redis zu verwenden. Wenn wir nun den State aus unserem Backend verlagert haben, kann dieses beliebig skaliert werden – mit dem State haben wir potenziell aber auch das Bottleneck verlagert.
Wir führen das Beispiel von oben weiter und speichern die Session (Warenkorb) in einer Datenbank. Unser E-Commerce-System gewinnt an Beliebtheit, und bei jedem Newsletter mit dem nächsten Sonderangebot bleiben unsere Anfragen an der Datenbank hängen, weil diese einfach nicht genug Kapazität hat. Gemäß dem CAP-Theorem können wir unsere Datenbank nicht beliebig skalieren und gleichzeitig die Konsistenz der Daten garantieren [2]. Eine Datenbank kann auf zwei verschiedene Arten skaliert werden: Replikation und Sharding.
Die Replikation ermöglicht, die Daten hochverfügbar auf mehrere Instanzen als Kopie zu halten. Das heißt, wir können eine Datenbank auf drei Systeme replizieren. Wenn wir nun ein Update an unserem Warenkorb vornehmen, muss die Datenbank sicherstellen, dass die Änderungen auf allen drei Systemen erfolgt. Je nach Performanceansprüchen kann die Transaktion beendet werden, wenn ein System die Änderung speichert. Man spricht von „Eventual Consistency“, da alle Anfragen auf die anderen zwei Systeme die Änderungen zu diesem Zeitpunkt noch nicht beinhalten. Wenn wir einem Kunden, nachdem er ein Produkt zum Warenkorb hinzufügt, einen leeren Warenkorb anzeigen, weil die Abfragen auf verschiedenen Systemen unsere Datenbank ausgeführt wurde, wäre das sehr verwirrend. Wir möchten also die Konsistenz der Daten garantieren.
Die zweite Möglichkeit zu skalieren, bietet Sharding. Mit Sharding verteilt man die Daten auf mehrere Systeme anhand von einer bestimmten Eigenschaft, beispielsweise einem Sharding Key. Sharding ist eine Shared-nothing-Architektur für Datenbanken zur Steigerung der Performance. Wir können unsere Warenkorbtabelle nach Benutzer auf verschiedene Systeme verteilen – Benutzername als Sharding Key –, und z. B. unsere Produkte im Shop ebenfalls auf die verschiedenen Systeme verteilen – Artikelnummer als Sharding Key. Wenn wir nun aber zu unserem Warenkorb die Produkte anzeigen möchten, müssen wir die zwei Tabellen joinen. Da wir unsere Daten auf mehrere Systeme verteilen, muss ein solcher Join ebenfalls über die verschiedenen Systeme verteilt werden. Das kostet sehr viel Performance.
Wenn unsere Ansprüche an die Konsistenz nicht zeitsensitiv sind, kann die „Eventual Consistency“ die Skalierbarkeit eines Systems erhöhen. Content Management bietet sich hier als gutes Beispiel an. Wenn wir einen neuen Artikel schreiben, muss dieser nicht innerhalb von Nanosekunden für die Benutzer ersichtlich sein. Dieser Anspruch erlaubt uns, den Artikel an alle unsere Content Management Backends per Replikation zu verteilen.
Besteht eine Architektur aus verschiedenen Services, so stellt sich die Frage, wie man diese am besten verteilt. Stellt man für jeden Service eine eigene Maschine bereit, benötigt man hierfür sehr viele, und es kann sein, dass viele von ihnen nur wenig Last erhalten, d. h. die Nutzung der Ressourcen ist nicht effektiv. Ein Architekturmuster zur Vermeidung von Unterlast ist die Cookie-Cutter-Architektur [3]. Hierbei werden alle Services auf einem Image provisioniert, das dann horizontal skaliert wird. So ist es dann möglich, die Last auf eine optimale Anzahl von Maschinen zu verteilen. Allerdings hat dieses Muster nicht nur Vorteile, denn alle Services müssen denselben Technologiestack nutzen, und dies erzeugt Abhängigkeiten unter den Services, die wir sonst nicht hätten.
Das Bridgehead-Pattern haben wir selbst erfunden bzw. haben es selbst erfinden müssen. Wenn wir mit großen proprietären E-Commerce-Frameworks arbeiten, sind diese skalierbar, aber nicht dynamisch. Zudem ist die Skalierung mit erheblichen Lizenzkosten verbunden, sodass es sich für den Auftraggeber nicht rechnet, die theoretische Maximalkapazität zu lizenzieren. Zu bestimmten Zeitpunkten wird diese Maximalkapazität jedoch erreicht, man denke nur an das Weihnachtsgeschäft oder den berüchtigten Cyber Monday. Oft überlegt sich das Marketing dann auch zu just diesem Zeitpunkt, die besten Angebote des Jahres zu veröffentlichen, die große Scharen von Käufern anlocken. Ein Workaround um diese restriktiven Lizenzmodelle ist eine Entkoppelung dieser Spezialangebote vom Rest des Systems. Man baut dafür dann eine minimale Shared-Nothing-Umgebung mit asynchronem Bestellprozess. Dabei wird die Bestellung des Kunden aufgenommen, die Bezahlung findet jedoch später statt, wenn das System erneut Kapazität hat. Man kann den Käufer beispielsweise per E-Mail informieren, dass er nun seine Zahlungsdetails angeben darf. So kann man die Last auf das System steuern und eine ausgezeichnete User Experience bieten (very snappy), verliert jedoch unter Umständen die Gruppe der Impulskäufer. Ein kleiner Preis dafür, dass das System überleben kann, finden wir.
Nun sind wir soweit, unser CDN ist international verteilt, das Backend ist stateless, die Datenbank kann mittels Sharding skaliert werden und unsere Integrationen sind asynchron: Wir sind bereit für den nächsten Cyber Monday. Nicht ganz! Wir müssen noch sicherstellen, dass die entsprechenden Systeme von der Infrastruktur bereitgestellt werden. Für den Cyber Monday können wir die Systeme eine Woche vorher bestellen und die Zeit für das Set-up antizipieren. Beim Slashdot-Effekt haben wir diese Chance nicht. Automatische Skalierbarkeit der Infrastruktur ist der nächste Schritt, um auch bei spontanen Traffic Bursts die Verfügbarkeit der Seite zu gewährleisten. Es gibt viele verschiedene Möglichkeiten, automatisch neue Systeme zu provisionieren. Als Grundvoraussetzung definieren wir die folgenden Punkte: Configuration Management zur Nachvollziehbarkeit und Wiederholbarkeit eines System-Set-ups, automatisches Deployment der Applikation, dynamisches Load Balancing und ein Virtualisierungslayer mit API. Erst wenn wir Systeme so bereitstellen, dass wir sie jederzeit zerstören und automatisch neu aufbauen können, kann die Infrastruktur unsere Skalierung ermöglichen.
Qualitätsszenarien zur Skalierbarkeit beschäftigen sich hauptsächlich mit den Auswirkungen des Hinzufügens oder Wegnehmens von Ressourcen, und die Messungen reflektieren die Änderungen an Last und Verfügbarkeit [1]. Wie bei allen Qualitätsszenarien ist die Verständlichkeit für alle beteiligten Parteien die Leitidee, damit die schließlich resultierende Architektur fair verhandelt werden kann. In einem ersten Beispiel könnte ein Verhandlungsergebnis sein, dass das System nicht automatisch skalieren kann, weil beispielsweise das erforderliche Budget hierfür nicht ausreicht oder die benötigten Kompetenzen in Entwicklung und Betrieb nicht vorhanden sind. Denn wie wir oben festgestellt haben, braucht es viel, um echte Automation sicherzustellen: Möchte das Marketing an bestimmten Tagen besondere Promotionen anbieten, so ist dies mindestens einen Monat vorher mit Architektur und Betrieb abzustimmen, damit zum Zeitpunkt der Promotion ausreichend Kapazität verfügbar gemacht werden kann.
Ein weiteres Beispiel beschäftigt sich mit dem potenziell viralen Effekt von Marketingkampagnen. Hier ist vorher nicht absehbar, welche Last das System aushalten muss. Beispielsweise habe ich diese Woche einen ungebetenen Newsletter von Adobe erhalten, offenbar der erste seiner Art von einem neuen System. Bei dem Versuch der Abmeldung war das Zielsystem offenbar vom Ansturm der Unsubscriptions so überwältigt, dass es nichts außer weiße Seiten ausliefern konnte. Solche Vorfälle werfen ein schlechtes Licht auf die verantwortliche Gesamtorganisation. Verhandeln Sie also im Vorfeld beispielsweise folgendes Szenario, wenn alle einverstanden sind, dass der Traffic durch die Decke gehen könnte. Geben Sie jedoch hierfür im Vorfeld auch das zu erwartende Budget für die Lösung an. Seien sie dabei nicht zu konservativ, denn erfahrungsgemäß benötigt Autoskalierung pro Applikation einiges an Tuning und damit entsprechende Mehraufwände: Steigt die Anzahl der Besucher an, so soll das System die benötigte Kapazität selbstständig regulieren, sodass 90 Prozent der Page-Views in weniger als zwei Sekunden ausgeliefert werden können.
Bei diesem Szenario fällt auf, dass es unbounded ist. Je nach Betriebsmodell ist es ratsam, eine Obergrenze festzusetzen, denn sonst kann die Rechnung für die aufgewendete Bandbreite höher als erwartet ausfallen, insbesondere wenn das Marketing emotionale Erlebnisse mit Full-HD-Videos zu 50 MB das Stück bevorzugt.
Die Skalierbarkeit ist eines der wesentlichen Merkmale von Cloud-native-Anwendungen, benötigt jedoch Aufwände in Betrieb und Entwicklung, insbesondere dann, wenn es gilt, die Skalierbarkeit automatisch herstellen zu wollen. Bei E-Commerce-Anwendungen kann sich eine exakte Kalkulation der benötigten Betriebskosten jedoch lohnen, schließlich lassen sich Systeme nicht nur hoch-, sondern auch runterskalieren, sodass Einsparungen zum wirtschaftlichen Erfolg beitragen können.
Nicolas Bär arbeitet als IT-Architekt bei der Unic AG in Zürich. Als Performanceenthusiast optimiert er Webapplikationen und engagiert sich im Open-Source-Umfeld.
Daniel Takai ist Technologiemanager bei der Unic AG in Bern. Er ist dort für die Entwicklungsprozesse, Technologieentwicklung und Softwarearchitekturen verantwortlich.
[1] Bass et al.: „Software Architecture in Practice“, 3rd Edition, Addison-Wesley, 2012
[2] Gilbert, Seth; Lynch, Nancy: „Brewer’s conjecture and the feasibility of consistent, available, partition-tolerant web services.“, ACM SIGACT News 33.2 (2002), S. 51–59
[3] Cookie Cutter: http://paulhammant.com/2011/11/29/cookie-cutter-scaling/
„Big“ reicht nicht mehr, Daten müssen jetzt auch „fast“ sein. Oft geistert dazu der unbestimmte Begriff „Echtzeit“ durch Talks, Artikel und Technologiebeschreibungen. Doch was heißt Echtzeit eigentlich? Wikipedia weiß es, so mancher Technik-Evangelist aber anscheinend nicht. Um Echtzeit handelt es sich, wenn „bestimmte Ergebnisse zuverlässig innerhalb einer vorbestimmten Zeitspanne“ eintreffen.
Es reicht eben nicht, auf eine Anwendung Echtzeit zu schreiben wie ein ASAP in eine E-Mail. Denn es geht nicht darum, etwas so schnell wie möglich zu machen, sondern so schnell wie nötig. Und das ist ein großer Unterschied. Für einen Vertriebsmitarbeiter reicht es vollkommen aus, wenn Aufträge innerhalb von ein paar Sekunden in seiner Übersicht auftauchen. Für einen Ingenieur hingegen kann es entscheidend sein, dass eine Fräse ein Signal in genau dieser Mikrosekunde bekommt. Denn sonst produziert sie für den Schrott. Es kommt also auf die Anwendung an – wie bei fast allem in der Softwareentwicklung.
Die Frage, wie schnell etwas passieren soll, ist gerade in Big-Data- und und den oft damit verbundenen IoT-Anwendungen wichtig. Und das Frontend ist dabei das Ende einer langen Kette von Zeiteinschätzungen. So schnell wie manch Sensor messen kann, kann nicht jede Datenbank mithalten. Berechnungen wollen zum richtigen Zeitpunkt durchgeführt und Daten mit Metadaten angereichert werden. Ein Stau auf der Datenautobahn ist das Letzte, was Entwickler und Anwender wollen. Hier heißt es, einen genauen Blick auf Schnittstellen und die Performance zu richten. Nicht umsonst schreien solche Anwendungen geradezu nach verteilten Systemen, in denen sich Lastspitzen besser verteilen lassen.
Ein schnelles Big-Data-System muss wie ein gut geöltes Getriebe laufen, in dem die einzelnen Rädchen reibungslos ineinander greifen – auch wenn mal weniger los ist. Ein kniffliges Problem, dem sich unser Titelthema mit vier Artikeln annimmt. Die dort ausgeführte Lösung für das Geschwindigkeitsproblem heißt SMACK, was keine Frühstückszerealie ist, sondern ein Technologistack für schnelle Anwendungen mit großen Datenmengen. Sicherlich kommt Ihnen die ein oder andere Komponente von SMACK bekannt vor, aber das Zusammenspiel macht hier den Unterschied – ganz nach dem Motto „Ein Ganzes ist mehr als die Summe seiner Teile“. Dann passt das auch mit der Echtzeit.
Was SMACK außerdem so interessant macht? Nach einer knappen Dekade, in der MapReduce als Prinzip und das Hadoop-Ökosystem als entsprechender Werkzeugkasten den Ton angaben, richtet sich der Blick jetzt mehr auf den Datenfluss, weniger auf deren (langfristige) Persistenz. Doch lesen Sie selbst (ab Seite 34).
Machen sie Ihre Systeme also nicht einfach schneller, machen Sie sie schnell genug.
Das Web als neue Run-anywhere-Plattform ist gesetzt. Viele Unternehmen haben jedoch in der Vergangenheit in integrierte Anwendungen mit Desktoptechnologien investiert, wie Java Swing oder Microsoft WPF. In diesem Artikel wird ein Architekturansatz vorgestellt, der eine schrittweise Migration mit parallelem Betrieb und nahtloser Integration von neuen und alten Bestandteilen ermöglicht.
Java-Desktopanwendungen wurden vor dem Aufkommen von JavaFX klassischerweise mit Swing implementiert. Swing bietet ein betriebssystemunabhängiges API und eine reichhaltige Komponentenbibliothek, mit der sich Geschäftsanwendungen gut umsetzen lassen. In Abbildung 1 ist eine typische Java-Swing-Desktopanwendung zu sehen. Sie besteht aus einer Navigation und einem Formularbereich. Die Navigation erlaubt den Zugriff auf alle Anwendungsfälle, z. B. alles rund um Verträge. Statt einer Schnellnavigation könnte der Einstieg auch über ein Menü erfolgen. Im Formularbereich wird der jeweilige Anwendungsfall zur Verfügung gestellt. Im Anschluss an den Formularbereich sind unten verknüpfte Anwendungsfälle zu sehen.
Die Vorteile dieser Integration zeigen sich neben der starken Vernetzung der Daten und Anwendungsfälle auch an Punkten, die auf den ersten Blick weniger offensichtlich sind. Es ist lediglich eine Anmeldung erforderlich, und die Bedienkonzepte sind einheitlich. Außerdem gibt es einen gemeinsamen Kontext, in dem aggregierte Daten angezeigt werden können, z. B. auf der Kundenseite, wenn ein Mahnverfahren läuft.
Das User Interface im Swing Client ist sowohl für die grafische Aufbereitung als auch für die View-Logik verantwortlich. Oft fällt es schwer, die Grenzen zwischen Geschäftslogik und View-Logik scharf zu trennen, da beide ineinander greifen. Bei einer sauberen Trennung von Verantwortlichkeiten befindet sich auf dem Swing Client lediglich der Teil der Geschäftslogik, der eng mit der Darstellung verbunden ist. Im Beispiel könnte dies eine farbige Markierung von Feldern sein, die Daten mit Validierungsfehlern enthalten. Je nach Art der Validierung ist diese als Teil der reinen Geschäftslogik auf dem Server zu finden. Um die User Experience zu verbessern, kann auch ein Teil im Client redundant implementiert sein.
Zur Umsetzung des User Interface mit View-Logik kommt in der Regel das Entwurfsmuster Model View Controller (MVC) zum Einsatz: Swing-Komponenten selbst sind intern bereits nach dem MVC-Muster aufgebaut, aber auch bei dem übergreifenden Design von grafischen Anwendungen hat sich der Einsatz des MVC-Musters bewährt. Es gilt daher zwischen MVC auf Anwendungsebene und auf Komponentenebene zu differenzieren.
Bei Swing erfolgt die Integration verschiedener Anwendungsteile durch entsprechende Implementierung der Dialoge und Verknüpfung innerhalb der View-Logik. Da der Client einen eigenen Zustand und eine eigene Ablauflogik hat, lassen sich damit Anforderungen wie Datenübergabe zwischen Dialogen und abwechselnde Bearbeitung verschiedener Anwendungsfälle abbilden. Letzteres ist wichtig, wenn ein Bearbeiter in einem Vorgang durch ein synchrones Ereignis, beispielsweise einen Telefonanruf, unterbrochen wird. Dann soll es möglich sein, den anrufenden Kunden zu bedienen und danach an der Stelle weiterzumachen, an der der Bearbeiter vor dem Telefonanruf war.
Diese hohe Integration hat ihren Preis: Im Vergleich zu mehreren isolierten kleineren Anwendungen ist der langfristige Aufwand höher. Neben einer umfangreicheren Architektur für das reibungslose Zusammenspiel der Anwendungsteile ist die Entwicklung neuer Features aufwendiger. Auch bei der Wartung eines solchen Frontend-Monolithen entstehen allein durch Abstimmung und Test des insgesamt komplexeren Systems höhere Aufwände. Dem höheren Entwicklungsaufwand stehen die gute Bedienbarkeit, Zeitersparnis bei der Nutzung und höherer Zufriedenheit der Anwender gegenüber. Bei einem Wechsel der Plattform von Swing-Desktop zu Webanwendungen gibt es daher oft die Anforderung, weiterhin die gute Benutzbarkeit eines integrierten Frontends bereitzustellen.
Als Konsequenz von clientseitigem JavaScript gepaart mit Ajax und leistungsfähigen Browsern entstehen heute moderne Webanwendungen, die Desktopanwendungen sehr nahe kommen. Häufig auch als Single-Page-App bezeichnet, kommt bei solchen Anwendungen kein Neuladen der Seite mehr vor. Dabei stellt die Serverseite eine Schnittstelle mit HTTP als Kommunikationskanal bereit. Als Datenformat kommt häufig JSON oder XML zur Anwendung. Optional lässt sich dies mit Verfahren kombinieren, die Daten aktiv zum Client pushen können.
Eine solche Architektur hat den Vorteil gegenüber klassischen Webarchitekturen mit serverseitigem Benutzerinterface, dass eine deutlich stärkere Trennung zwischen Client, Server, Daten und Präsentation besteht und die Kommunikation sowohl von der Plattform als auch vom Framework unabhängig ist. Mit diesem Ansatz lassen sich leicht verschiedene Clients entwickeln, die dieselbe Schnittstelle verwenden. Diese Architektur ist damit näher an Desktoparchitekturen, die auch Datenschnittstellen nutzen. Beispiele für den sinnvollen Einsatz unterschiedlicher Clients sind separate mobile Anwendungen oder auch das Bereitstellen von Teilfunktionalität für Endkunden in einer separaten Anwendung.
Für die Entwicklung von integrierten und auf Webstandards basierenden Browseranwendungen hat sich neben den Platzhirschen AngularJS [1] und ReactJS ein reichhaltiges Ökosystem an Frameworks entwickelt. Erfahrungsgemäß kommen Java-Entwickler gut mit den Konzepten von AngularJS zurecht. Wie in Abbildung 3 zu sehen ist, hat diese Architektur noch einen weiteren Vorteil: Die Integration verschiedener Dienste kann im Client erfolgen. Das erlaubt Optimierungen (z. B. Caching oder Lazy Loading) und verringert die Last im Backend.
Ablöseprojekte und Neuentwicklungen im Big-Bang-Verfahren tendieren dazu, über Zeit- und Kostenrahmen hinaus zu laufen. Erschwerend kommt hinzu, dass dabei das neue System erst spät in Produktion geht. Fehler und Verbesserungspotenzial werden somit ebenso spät erkannt, notwendige neue Features müssen eventuell doppelt neu entwickelt werden. Das steht im Gegensatz zu dem Wunsch, früh Feedback von echten Nutzern einzuholen und das System kontinuierlich weiterentwickeln und verbessern zu können.
Aus diesen Gründen ist eine schrittweise Ablösung wünschenswert – am besten ohne Medienbruch, sondern als nahtlos integrierte Anwendung. Es gilt also einen Weg zu finden, ein Frontend mit Webtechnologie mit dem Swing-basierten Frontend zu integrieren. Die Herausforderung besteht dabei nicht nur darin, dass eine Webseite innerhalb der Anwendung angezeigt werden muss. Es gilt auch Interaktionen zwischen dem Desktopteil und dem Webteil zu realisieren, denn verknüpfte Anwendungsfälle müssen in beide Richtungen realisiert werden. Ein möglicher Lösungsweg geht dabei über JavaFX.
JavaFX gehört zum Standardlieferumfang der Oracle-Java-Laufzeitumgebung und steht somit auf allen aktuellen Plattformen zur Verfügung. Außerdem hat Oracle JavaFX als Open Source bereitgestellt, sodass keine Bedenken bezüglich der Zukunftssicherheit aufkommen. JavaFX bietet zwei wichtige Funktionalitäten: Zum einen lassen sich JavaFX-Komponenten gemischt mit Swing-Komponenten einsetzen. Zum anderen bietet JavaFX eine auf der WebKit Engine basierende Browserkomponente, die WebView [2]. Mit der WebView lässt sich die Darstellung von Webinhalten direkt in eine Swing-Anwendung einbetten. Es werden lediglich wenige Zeilen Java benötigt, um Google innerhalb einer Swing- oder JavaFX-Anwendung einzubetten. Das Ergebnis ist in Abbildung 4 zu sehen:
final WebView webView = new WebView();
final WebEngine webEngine = webView.getEngine();
webEngine.load("https://www.google.com/");
Natürlich wird eine Rahmenanwendung benötigt, die die Swing-Komponenten initialisiert und aus der die WebView eingebunden wird. Innerhalb der WebView ist bereits eine normale Verwendung der Webanwendung möglich: Links lassen sich anklicken, Formulare ausfüllen und absenden.
Was jetzt noch fehlt, ist eine Brücke zwischen Webinhalten und der Rahmenanwendung. Das Ziel bei der Architektur der Verbindung sollte sein, dass die Integration in Swing nicht zwingend ist, sondern die Webanwendung auch losgelöst betrieben und genutzt werden kann.
Dazu bietet sich JavaScript an: Zum einen kann von Java heraus JavaScript in der jeweiligen Seite ausgeführt werden, zum anderen können Java-Methoden als JavaScript publiziert und dann von der Seite heraus aufgerufen werden. Das API erlaubt auch weitergehende Eingriffe, wie Modifikationen der zu ladenden CSS- und JavaScript-Dateien sowie Modifikationen des DOM der Seite selbst. Damit lässt sich die Webseite für die eingebettete Nutzung optimieren und das Styling anpassen oder zusätzliche Interaktionselemente hinzufügen.
Im einfachsten Fall publizieren wir ein Java-API. Dabei ist darauf zu achten, dass die Seite geladen ist, damit die Registrierung fehlerfrei funktioniert. An dieser Stelle wird auch deutlich, dass eine Single-Page-App sich besonders gut eignet, da diese lediglich einmal geladen wird. Der zugehörige Quellcode zur Veranschaulichung enthält die Schritte:
Registrierung eines Callbacks, um den Ladestatus der Seite zu ermitteln
Reaktion des Callbacks, wenn der Ladestatus ergibt, dass die Seite geladen ist
Aus dem Callback erfolgt die Publizierung der Java-Klasse JavaApp als JavaScript-app-Objekt
Die Java-Klasse ist einfach gehalten und zeigt lediglich eine Swing Alertbox an. Die grundsätzliche Verwendung sieht wie folgt aus:
public static class JavaApp
{
public void trigger()
{
JOptionPane.showMessageDialog(null, "Java Dialog triggered from the web!");
}
}
Das Beispiel für die Registrierung der Java-Klasse verwendet Java-8-Lambda-Ausdrücke, um den Listener zu implementieren:
webEngine.getLoadWorker().stateProperty().addListener(
(ObservableValue<? extends State> ov, State oldState, State newState) ->
{
if (newState == State.SUCCEEDED)
{
JSObject win = (JSObject) webEngine.executeScript("window");
win.setMember("app", new JavaApp());
}
});
In Abbildung 5 ist der geöffnete Swing-Dialog als Reaktion auf den Knopfdruck in der WebView zu sehen.
Die Integration selbst ist keine große technische Herausforderung. Wünschenswert wäre jedoch ein Vorgehen, bei dem die notwendigen Änderungen beim schlussendlichen Sprung aus der Swing-Desktopanwendung ins Web minimiert werden. Die Lösung dazu könnte wie folgt aussehen: Es wird ein API zur Integration designt, die sich sowohl mit der Swing-Rahmenanwendung als auch mit einer (späteren) Webanwendung abbilden lässt. Der Umfang des API wird dabei gering und das Abstraktionsniveau hoch gehalten, schließlich soll hier keine zweite Controller-, sondern eine Integrationsschicht entstehen. Die Java-Anwendung ist dabei auch in der Lage Legacy-APIs als lokale HTTP-Dienste anzubieten, um auch an dieser Stelle eine schrittweise Migration zu ermöglichen. Mit dieser Architektur ist ein paralleler Betrieb von Swing- und JavaScript-Rahmenanwendung möglich. Das erlaubt frühes Testen, schnelles Feedback und je nach Anwendung auch eine stufenweise Migration unterschiedlicher Benutzergruppen.
Bei der Rahmenanwendung im Web lässt sich dabei frei zwischen den verfügbaren JavaScript-Frameworks und den zu nutzenden Verfahren zur Gestaltung der Datenflüsse wählen. In die Auswahl sollte dabei miteinbezogen werden, dass sich manche Frameworks, wie AngularJS, aufgrund der verwandten Konzepte für Java-Entwickler eher anbieten, als solche mit ungewohnten Mustern. Das folgende Beispielprojekt ist mit AngularJS 1.4 umgesetzt und lässt sich sowohl per Swing als auch nur mit einem Webbrowser nutzen.
Zur Veranschaulichung gibt es eine beispielhafte Umsetzung als Maven-Projekt unter [3]. Die Klasse SwingIntegration liefert dabei alles, was man braucht: Einen lokalen Webserver zur Simulation des Backends, eine Swing-basierte Rahmenanwendung und die JavaScript-Anwendung zur Integration auf dem Desktop. Im Beispiel kommen AngularJS und Bootstrap zum Einsatz. Diese werden in diesem Fall aus dem Internet nachgeladen. Nachdem die Klasse gestartet wurde, kann parallel unter dem folgenden URL per Browser die pure JavaScript-Anwendung genutzt werden: http://localhost:8081/sample.html.
Wenn man den Zugriff über Browser und über die Swing-Anwendung vergleicht, bemerkt man unterschiedliche Einträge unter dem Punkt Invoices – dies demonstriert die Implementierung des Datenzugriffs über Swing bzw. JavaScript. Im Java-Code ist dies die Methode getInvoices(), die für die WebView exportiert wird. In der nativen Browserversion werden die Daten durch den InvoiceService direkt bereitgestellt.
Listing 1: Zugriff auf die Invoices im AngularJS-Service
app.factory('InvoiceService', function () {
var api = {
getInvoices: function () {
var invoices = [];
var invoice = {number: "acme-2", date: "2015-11-01", companyId: 1, paid: false };
invoices.push(invoice);
return invoices;
}
};
if (window.appFrame) {
api.getInvoices = function () {
var data = window.appFrame.getInvoices();
return JSON.parse(data);
};
}
return api;
});
Der Zugriff auf die Contracts erfolgt durch Ajax-HTTP-Zugriffe, sowohl im Browser als auch in der Swing-Anwendung. Unter Contracts wird der Rückweg aus dem Browser zum Swing demonstriert, durch den Knopf Send email wird auf dem Desktop ein Swing-Dialog geöffnet, im Browser ein HTML-Dialog.
Mit den gezeigten Mitteln ist damit eine schrittweise Migration von typischen Enterprise-Anwendungen möglich. Die konkrete Umsetzung ist im Wesentlichen auf zwei Wegen denkbar:
Möglichst schnelle Umsetzung von (teil-)autonomer Funktionalität als Webanwendung. Diese kann dann auf jedem webfähigen Endgerät genutzt werden und erlaubt unter Umständen schon ganzen Nutzergruppen, auf die Desktopanwendung zu verzichten.
Langfristiger Einsatz einer Desktoprahmenanwendung. Hier ist zwar das Ziel, schon kurzfristig von den Möglichkeiten einer Webanwendung zu profitieren und eine langfristige Migration ins Web zu erzielen, jedoch sind die Webanteile zwingend auf die Rahmenanwendung angewiesen und nicht separat lauffähig.
Ob man sich für den Weg entscheidet, einzelne Teile bereits als reine Webanwendung autonom betreiben zu können, oder bis zum Schluss eine Rahmenanwendung beibehält, hängt stark davon ab, ob ein Teilbetrieb im Web sinnvoll ist und auch in welchem Zeitraum die Migration durchgeführt werden soll. Lassen sich bereits einzelne Anwendungsfälle mit der reinen Webtechnologie nutzen, hat man die Chance auf frühes Nutzerfeedback: Stimmen z. B. Performance, Bedienkonzepte und die Nutzung auf Geräten mit unterschiedlichen Bildschirmformaten und -auflösungen? Auf jeden Fall sollte man einplanen, dass nach mehreren Jahren Nutzung der Desktopanwendung die Umstellung ins Web für den einen oder anderen Anwender ein kleiner Kulturschock ist. Umso wichtiger ist die Möglichkeit durch einen frühzeitigen und engen Dialog mit den Anwendern nachsteuern zu können. Die Abbildungen 7 und 8 zeigen exemplarisch die Phasen einer Enterprise-Java-Anwendung bei der Migration auf dem Weg von einer reinen Swing-Anwendung hin zu einer Webanwendung.
Das unterschiedliche Styling, durch das ein optischer Bruch entsteht, ist für dieses Beispiel bewusst gewählt worden. In der Praxis können hier Anpassungen über CSS erfolgen, die ein harmonisiertes Erscheinungsbild ermöglichen.
Die Anwendung ist nach dem typischen Model-View-Controller-Muster aufgebaut. Die bidirektionale Integration zwischen Swing und Web wird zum einen deutlich, wenn zwischen den verschiedenen Funktionen der Anwendung umgeschaltet wird. Zum anderen reagiert die Statusleiste im unteren Bereich der Anwendung auf Aktivitäten in der WebView. Auch der Dialog zum E-Mail-Versand, der als Swing-Formular angezeigt wird, kann nahtlos aus einer Aktion im Webanteil aufgerufen werden.
Vorteil der sanften Migration ist, dass auch das Unternehmen Erfahrungen mit Webtechnologie schrittweise sammeln kann. Der Zwang im ersten Anlauf gleich alles richtig zu machen, entfällt damit. Neben der hohen Entwicklungsgeschwindigkeit von JavaScript-Frameworks spielt auch der Faktor Mensch eine wichtige Rolle und profitiert von entsprechenden zeitlichen Puffern. Gerade im Java-Enterprise-Umfeld ist Webentwicklung – allem voran JavaScript – oft mit gewissen Vorbehalten belastet: JavaScript sei keine ernstzunehmende Sprache, es fehle ein statisches Typensystem, es gäbe keine professionelle Werkzeugunterstützung und testgetriebene Entwicklung mit Refactoring sei nicht machbar. Mit dem Google Web Toolkit ist jedoch die Entwicklung mit den gewohnten Werkzeugen in der gewohnten Sprache möglich.
Die Herausforderungen beschränken sich jedoch gerade im Enterprise-Umfeld nicht nur auf bestehende Anwendungen und große Codebasen, die weiter entwickelt werden wollen, sondern es gibt auch bestehende Mitarbeiter mit entsprechenden Skills. In manchen Firmen haben Mitarbeiter nicht nur Vorbehalte, sondern echte Schwierigkeiten damit, außerhalb der gewohnten Java-Welt zu entwickeln. Die erlebte Herausforderung, sich mit JavaScript, HTML und CSS zu befassen, ist sicherlich individuell unterschiedlich stark ausgeprägt. Eine dauerhaft tragfähige Lösung ist die wie auch immer geartete Abstraktion oder gar Isolation der Webwelt von Java-Entwicklern jedoch nicht. Auch dafür lässt sich der in diesem Artikel vorgestellte Ansatz optimal nutzen: Während Entwickler, die Webtechnologien als spannende Chance sehen, sich weiter zu Pionieren entwickeln werden, können sich eher konservative Entwickler um Wartung und Weiterentwicklung des in Java entwickelten Anwendungsteils kümmern.
Auch die Architekten müssen sich etwas umgewöhnen. Anders als in der Enterprise-Java-Welt gilt es vorsichtig damit umzugehen, eigene Frameworks zu entwickeln. War man in der Java-Welt eigentlich erst dann als echtes Enterprise-Projekt ernst zu nehmen, wenn man ein eigenes Persistenz- und Logging-Framework entwickelt hatte, kann sowas in der Webwelt dazu führen, dass keine erfolgreiche Evolution von Architektur und Technologie möglich ist. Entsprechend vorsichtig sollte hier vorgegangen werden. Optimaler Weise ist eine möglichst lose Kopplung von gewähltem Framework zur Frontend- und Geschäftslogik im Frontend vorgesehen. Anders als das langfristig stabile Swing ist hier von einer anderen Änderungsfrequenz auszugehen. Auch wenn heute eine Eigenentwicklung in Qualität und Funktionsumfang vorhandene Lösungen übertrifft, kann nächste Woche schon ein neuer Open-Source-Stern aufgehen. Dann sollte man in der Lage sein, sich dem anzupassen – nicht zuletzt auch aus wirtschaftlichen Gesichtspunkten.
Ist man sich dessen bewusst, dass es im Enterprise-JavaScript-Umfeld zu schnellen Entwicklungen kommt, und plant dies entsprechend ein, kann eine passende Abstraktionsschicht zwischen Fremdbibliotheken und eigener Anwendung sehr wohl sinnvoll sein: Änderungen unter der Haube, z. B. der Austausch des Build-Systems, haben optimaler Weise keinen Einfluss auf Entwickler mit dem Fokus auf die Anwendungsentwicklung. Durch bewährte Muster wie Komposition statt Vererbung, Dependency Injection und Vorgehensweisen wie Test-driven Development ist auch in der JavaScript-Welt eine langfristig wartbare und erweiterbare Anwendung möglich.
Die vorgestellte Architektur ist auf andere Plattformen, wie .NET oder native Anwendungen, analog übertragbar. In den Fällen wird dann zwar eine andere technische Implementierung benutzt, die Konzepte und auch APIs sind jedoch ähnlich. Ein Beispiel für eine andere Implementierung ist das Chromium Embedded Framework, kurz CEF [4]. Von CEF gibt es eine Java-Portierung namens JCEF, die mittels JNI arbeitet und keine JavaFX-Abhängigkeit besitzt. Der Integrationsansatz ist dabei nicht nur auf Migrationsprojekte beschränkt: Es gibt viele Nutzer von CEF, die damit Anwendungen entwickeln, die von den Möglichkeiten beider Welten profitieren. In der Liste der CEF-Nutzer finden sich namhafte Beispiele wie Spotify, Amazon Music und der Stream-Client [5].
Der vorgestellte Ansatz erlaubt eine sanfte Migration hin zu einer API-gestützten Architektur, die nicht nur Desktop und Browser als Clients unterstützt. Durch HTTP-Endpunkte ist es leicht möglich, Dienste für externe Partner bereitzustellen oder mobile Clients mit nativen Anwendungen zu unterstützen. Ebenso bietet eine Architektur mit definierten HTTP-APIs die Möglichkeit, unterschiedliche Anwendungen für verschiedene Nutzergruppen bereitzustellen: Während der Mitarbeiter im Backoffice ein Expertensystem nutzt, steht ein Wizard-System für Endkunden im Internet bereit.
Die Herausforderungen bei der Migration sollen jedoch nicht verschwiegen werden: Um eine nahtlose Integration zu realisieren, sind Punkte wie eine gemeinsame Anmeldung und Abmeldung für alle Bestandteile der Anwendung zu lösen. Bei einem angenommenen Zeithorizont von Jahren ist der damit einhergehende Aufwand für redundante Implementierung von Funktionalität und Querschnittsfunktionalität einzuplanen.
Insgesamt stellt sich die Frage, in welcher Reihenfolge die Funktionalitäten der Anwendung migriert werden sollen. Als Anhaltspunkte können da verschiedene Kriterien dienen: Welche Teile werden in der nächsten Zeit weitere Entwicklung erfahren und können dann sinnvoll in die neue Welt verlegt werden? Soll eine autonome Nutzung im Browser frühzeitig möglich sein, und welche Grundfunktionen sind dafür erforderlich?
Auch lassen sich Bedienkonzepte von sehr reichhaltigen Desktopanwendungen oft nicht sinnvoll eins zu eins auf eine Webanwendung übertragen. Sollen diese beiden Welten nahtlos miteinander verbunden werden, kann es erforderlich sein, die bestehenden Bedienkonzepte zu hinterfragen und für eine Nutzung im Web zu harmonisieren.
Im Test ist aufgefallen, dass die Anwendung unter Windows und OS X optisch einwandfrei war, unter Linux hingegen Artefakte zu sehen waren. Hier bleibt zu hoffen, dass Oracle mit zukünftigen JavaFX-Versionen Abhilfe schafft.
Thomas Kruse hat Informatik studiert und ist als Architekt, Berater und Coach für die trion development GmbH tätig. In seiner Freizeit engagiert er sich im Open-Source-Umfeld und leitet die Java Usergroup in Münster.
[1] AngularJS: https://angularjs.org/
[2] Java FX WebView: http://docs.oracle.com/javafx/2/webview/jfxpub-webview.htm
[3] Repository zum Artikel auf GitHub: https://github.com/trion-development/java-web-bridge
[4] CEF: https://bitbucket.org/chromiumembedded/cef
[5] CEF-Anwendungen: https://en.wikipedia.org/wiki/Chromium_Embedded_Framework#Applications_using_CEF