January 23, 2015
Pimp my Legacy Webapp! Integration von AngularJS und Struts1
Architekturvergleich
Architektur serverseitiger Webframeworks
Bei serverseitigen Webframeworks wie Struts und JSF, aber auch PHP befindet sich sehr viel Logik auf dem Server. Benutzeranfragen werden auf dem Server ausgewertet, dann werden mittels Templating Mechanismen, wie JSP, HTML-Seiten auf dem Server generiert und an den Client gesendet. Der Zustand jedes Benutzers befindet sich auf dem Server, was bei großen Nutzerzahlen eine Beeinträchtigung der Performance ist. Aktuellere Frameworks wie JSF 2 bieten Support für AJAX Features, allerdings bleibt auch hier die Problematik der Skalierung auf große Benutzerzahlen aufgrund des zustandsbehafteten Servers.
Architektur von Single Page JavaScript-Apps
Bei Single Page-Apps gibt es klassisch nur noch einen vollen Page-Request zum Server beim Laden der Webseite (die eigentlich schon eine Webanwendung ist). Alle anderen Aktionen sowie die Navigation werden komplett via JavaScript gesteuert, wodurch sich ein flüssigeres, einer nativen Anwendung ähnliches, Benutzererlebnis ergibt. Moderne Browser beinhalten hoch performante JavaScript-Engines (z.B. V8 [3] in Chrome). Daher bietet es sich an, Anwendungslogik in den Client, d.h. in den Browser zu verlagern. Dies spart Server-Ressourcen und ermöglicht hohe Benutzerzahlen mit vergleichsweiser geringer Hardwareausstattung des Servers.
Single Page-Apps halten zumeist den Zustand der Anwendung im Client und arbeiten daher oft mit zustandslosen Servern, die als reine Datenlieferanten dienen. State-of-the-Art sind hierfür aktuell REST [4] Schnittstellen.
Teilmigration einer Struts-Anwendung
Im Folgenden wird eine Möglichkeit der Teilmigration von Legacy-Anwendungen am Beispiel einer Struts1-Anwendung gezeigt. Struts1 war zu Beginn der 2000er ein populäres Webframework im Java-Umfeld. Alle Erkenntnisse lassen sich aber auch auf andere serverseitige Webframeworks übertragen.
Normalerweise liefert Struts1 auf eine Anfrage als Antwort eine fertig gerenderte HTML-Seite. Abb.1 zeigt vereinfacht den Ablauf eines Requests in Struts. Auf die Anfrage des Browsers wird im Server eine „Action“-Klasse aufgerufen, welche Daten von der Business-Schicht lädt und diese in eine „Form“ schreibt. Die Inhalte der „Form“ werden von Struts dann via JSP in HTML-Seiten geschrieben und an den Client gesendet. Bei jedem neuen Request wird dieser Zyklus neu durchlaufen. Dadurch wird die Anwendung sehr schwerfällig und es werden viele redundante Daten an den Client gesendet.
Möglichkeiten der Integration von AngularJS
Will man nun die Oberfläche einer Legacy-Webapp zumindest teilweise durch AngularJS ersetzen, dann ist wie oben beschrieben, eine REST-artige Schnittstelle auf dem Server vorgesehen. Die technologisch sauberste Lösung wäre die komplette Umstellung der Client-Server-Kommunikation auf eine REST-Schnittstelle. Im Java Umfeld stehen dafür verschiedene Frameworks, wie z.b. Jersey [5] zur Verfügung. Die Umstellung von einem zustandsbehafteten Server auf eine zustandslose REST-Schnittstelle ist jedoch sehr zeitaufwändig, risikoreich und teuer.
Ein alternativer Ansatz ist die Integration der AngularJS Client-Server-Kommunikation in die bestehende Architektur. In diesem Fall kann die Anwendung schrittweise nach AngularJS migriert werden. Der bestehende Business-Code kann ohne größeren Aufwand wiederverwendet werden. Dasselbe gilt auch für Features der bestehenden Anwendungsarchitektur. Bei Struts1-Anwendungen sind hier unter anderem Sicherheit und Validierung relevant. Ein kleiner Nachteil ist allerdings, dass der Zustand bei einer ersten Migration im Server bleibt.
Dieser alternative Ansatz wurde in einer Struts1-Anwendung umgesetzt und steigerte die Benutzerzufriedenheit deutlich. Im Folgenden werden die wichtigsten Migrationsschritte erklärt. Um die Client-Server-Kommunikation von der Auslieferung der HTML- und Javascript-Dateien zu trennen, wird eine 2-Teilung der Kommunikation durchgeführt. Auf die erste Anfrage des Browsers werden HTML-Dateien und JavaScript-Dateien zurückgegeben. Die JavaScript-Anwendung lädt dann die benötigten Daten vom Server über dieselbe URL nach.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@Override public ActionForward execute(final ActionMapping mapping, final ActionForm form, final HttpServletRequest request, final HttpServletResponse response) throws Exception { final String acceptType = request.getHeader("Accept"); final boolean jsonRequest = acceptType.startsWith("application/json"); if (false == jsonRequest) { return mapping.findForward("template"); } else { final String json = buildData(request, response, form); request.setAttribute("jsonResponse", json); return mapping.findForward("jsonResponse"); } } |
Listing 3
Wie im Listing1 zu sehen, wird in der „execute()“ Methode der Action eine Unterscheidung gemacht, ob die Anfrage eine „normale“ Struts-Anfrage ist oder ob nur Daten für die AngularJS-Anwendung geladen werden sollen. Um dies zu ermöglichen, nutzen wir aus, dass AngularJS bei der Anfrage automatisch den HTTP-Header Accept: application/json [6] mitsendet.
Bei der initialen Anfrage des Browsers (z.B. /show.do) wird, wie in der alten Anwendung ein HTML-Template mit zugehörigen JS-Dateien zum Client gesendet. In Listing 3 wird dafür das Mapping „template“ aufgerufen, welches alle benötigten Daten enthält. Wie dies im Detail von Struts gemacht wird, würde den Rahmen des Artikels sprengen und wird hier nicht erläutert.
1 2 3 4 |
$http({method: 'GET', url: '/show.do'}). success(<b>function</b>(data, status, headers, config) { $scope.data = data; }); |
Listing 4
Nachdem das Template im Browser geladen wurde, wird dort die JavaScript-Anwendung initialisiert. Diese verwendet den AngularJS-Service $http zur Kommunikation zum Server und ruft wiederum dieselbe Adresse „show.do“ auf (s. Listing 4).
1 2 |
<%@ page contentType="application/json; charset=UTF-8" pageEncoding="UTF-8"%> ${jsonResponse} |
Listing 5
Daraufhin werden in der Action nun alle Daten aus der Datenbank geladen und mittels dem Framework Gson [7] in JSON-Daten konvertiert. Gson kann ganze Java-Objektbäume in JSON-Strings serialisieren. Auch der Weg zurück funktioniert zuverlässig.
Die serialisierten JSON-Daten können nun als Antwort an den Client gesendet und dort auch weiterverarbeitet werden. Dafür wird der JSON-String, wie in Listing 3 zu sehen, an das Attribut „jsonResponse“ des HTTP-Requests gebunden und dann von Struts in das JSP-Template aus Listing 5 geschrieben. Das Resultat wird als Response an den Server gesendet und dort von AngularJS weiterverarbeitet.
Große Vorteile bringt diese Umstellung in Situationen, in denen nach dem initialen Laden der Templates und Daten nicht weiternavigiert wird. Im zugrundeliegenden Projekt wurden mit diesem Mechanismus alle Listenansichten migriert und mit mächtigen Web 2.0-Features versehen. Die Listenansichten sind nun mit modernen Tabellen ausgestattet und haben unter anderem eine Volltextsuche, die dem Benutzer direkt nach dem Tippen ein Feedback liefert. Dieses und weitere Features wären mit Struts1 oder jQuery nur sehr viel schlechter und komplizierter umzusetzen gewesen.
Fazit
Um die Kosten im Griff zu behalten und die Benutzer von den Möglichkeiten der neuen Technologien zu überzeugen, bietet sich eine Teilmigration von Legacy-Webapps an. Im ersten Schritt empfiehlt es sich, isolierte Ansichten, wie z.B. Listenansichten zu migrieren. Im nächsten Schritt kann man AngularJS auch nur in Teilen von HTML-Seiten einsetzen, um z.B. nur die hier nicht beschriebenen Validierungsmechanismen von AngularJS zu verwenden. Der nächste Schritt ist dann die Ersetzung der kompletten UI, wie hier im Artikel beschrieben. Wenn man dabei vorsichtig vorgeht, ist auch eine spätere Migration des Servers zu einer „echten“ REST-Schnittstelle im letzten Schritt umsetzbar.
Die hier beschriebene Vorgehensweise hat den Vorteil, dass man durch schrittweises Vorgehen den Kunden von den neuen Technologien und der gesteigerten Entwicklungsgeschwindigkeit überzeugen kann. Bestehende Logik im Server kann weitgehend wiederverwendet werden und senkt die Risiken und den Aufwand.
Ausblick
In diesem Artikel wurde darauf verzichtet, das Speichern von Daten zu beschreiben. Prinzipiell ist dies aber analog zum Laden, da die JSON Serialisierung mit Gson in beide Richtungen funktioniert. Auch hier bieten sich verschiedene denkbare Architekturen an. Speziell bei Struts1 können bei der Teilmigration bestehende Validierungsregeln im Server wiederverwendet werden.
Auch AngularJS konnte leider nur ganz grob umrissen werden. Konzepte wie „Services“ und „Direktiven“ bieten schöne Möglichkeiten zur Strukturierung von JavaScript-Anwendungen.
Ein weiteres interessantes Thema ist die Migration von Flash-Anwendungen nach AngularJS. Hingewiesen werden kann hier auf die AngularJS-Direktive w11k-flash [8], welche zur Integration von AngularJS und Flash verwendet werden kann. Eine ausführliche Anleitung findet sich hier [9].
Quellen
[1] Google Trends
[2] TodoMVC
[3] V8 JavaScript Engine
[4] Wikipedia
[5] Jersey.java.net
[6] AngularJS.org
[7] google.gson
[8] GitHub
[9] thecodecampus