Über unsMediaKontaktImpressum
Jan Blankenhorn 03. Dezember 2014

Pimp my Legacy Webapp! Integration von AngularJS und Struts1

Im privaten Umfeld und im Geschäftsalltag verbreiten sich moderne Webtechnologien immer rasanter. Bei der Wartung alter Webanwendungen, sprich "Legacy Webapps", gerät man deshalb unter den Druck, ähnliche Features sowie eine möglichst komfortable Benutzerführung bereitzustellen. Dies ist jedoch mit den vorhandenen Technologien gar nicht oder nur mit sehr hohem Aufwand möglich. Um die gewünschten Anforderungen umzusetzen, ist daher die Integration moderner JavaScript-Frameworks in Legacy Webapps unumgänglich.

Nach der Lektüre dieses Artikels sollten Ihnen die wichtigsten Unterschiede von Legacy Webapps und modernen JavaScript-Apps klar sein, damit auch Sie Ihre Legacy Webapps mit AngularJS pimpen können!

Warum nicht einfach jQuery verwenden?

jQuery ist nun seit einigen Jahren eines der beliebtesten JavaScript-Frameworks am Markt. Im Vergleich zu purem JavaScript bietet es auch bahnbrechende Neuerungen und steigert die Entwicklungsgeschwindigkeit enorm. jQuery in eine bestehende Webanwendung zu integrieren ist sehr einfach möglich, da einfach die benötigte jQuery-Bibliothek in den HTML-Code eingebunden werden muss. Die Architektur der Anwendung wird dadurch nicht beeinflusst. Die leichte Integration kommt daher, dass jQuery nur auf dem bestehenden HTML-Code arbeitet und gezielt DOM-Elemente manipuliert. Zumeist werden hier nur optische Anpassungen an bestehenden HTML-Seiten gemacht. Ein Beispiel sind die UI-Elemente von jQuery UI, mit denen sich schöne Dialoge und andere UI-Komponenten darstellen lassen. Die Steigerung der Benutzerfreundlichkeit kann mit jQuery sehr gut verbessert werden; die Entwicklungsgeschwindigkeit erhöht sich allerdings noch in unbefriedigendem Umfang.

JavaScript-MVC-Frameworks

Große Geschwindigkeitsvorteile bei der Entwicklung ergeben sich hingegen, wenn man eines der in den letzten Jahren immer populärer werdenden JavaScript-MVC-Frameworks einsetzt. Der prominenteste Vertreter dieser Gattung ist das von Google entwickelte AngularJS. Die Suchanfragen bei Google sind für AngularJS im Vergleich zu den konkurrierenden Frameworks deutlich höher [1]. Im Rahmen der Übersichtlichkeit wird hier auf einen Vergleich der konkurrierenden Frameworks verzichtet. Eine gute Übersicht findet sich aber auf der Webseite TodoMVC [2].

Das MVC-Prinzip ist ein alter Bekannter in der Softwareentwicklung und fand bisher hauptsächlich in der Rich-Client-Entwicklung Verwendung. Die Übertragung des MVC-Prinzips auf die Webentwicklung kommt jedoch einer kleinen Revolution gleich. Denn im Vergleich zu Frameworks wie jQuery, führen MVC-Frameworks aufgrund ihres höheren Abstraktionsniveaus zu deutlich höherer Entwicklungsgeschwindigkeit und besserer Codequalität. Aufgrund der klaren Trennung zwischen Model, View und Controller lassen sich zudem deutlich besser testbare Anwendungen erstellen.

Was ist AngularJs?

In AngularJS werden mit HTML-Code deklarativ die Oberflächen gestaltet. Ein wichtiger Aspekt ist, dass Anwendungslogik nur in den JavaScript-Dateien definiert wird. Dieser sollte weitmöglichst unabhängig von der Oberfläche sein. Im Gegensatz zu manch anderen MVC-Frameworks erweitert AngularJS den HTML-Code aber nur, anstatt ihn zu abstrahieren.

Um einen Bereich einer HTML-Seite von AngularJS verwalten zu lassen, muss dieser von einem Element mit dem Attribut ng-app umschlossen sein. Nach dem Laden der Seite sucht AngularJS nach dem Atrribut ng-app und analysiert den davon umschlossenen Bereich.

"Controller" werden in AngularJS mit dem Attribut ng-controller definiert. Controller und View kommunizieren über den "Scope", einen Container auf dessen Inhalt von beiden Seiten aus zugegriffen werden kann. Mit der {{}}-Notation kann mittels eines One-Way-Bindings im HTML auf Elemente des "Scope" zugegriffen werden. Die Daten können angezeigt, aber nicht zurückgeschrieben werden. Two-Way-Bindings ermöglichen auch das Schreiben in den "Scope" und können logischerweise nur an HTML-Input-Elementen verwendet werden.

Die Beispiele in Listing 1 und 2 zeigen eine simple AngularJS-Anwendung. In Listing 1 sehen wir die Definition der App myModule und des Controllers MyCtrl. In Zeile 2 wird dann über ein One-Way-Bindung die Länge der im "Scope" definierten Liste list ausgegeben. Danach folgt in den Zeilen 3-8 mit der AngularJS-Anweisung ng-repeat die Iteration über alle Elemente der Liste list. Innerhalb jedes <li>-Elementes wird der Inhalt der Property text jedes Listenelementes ausgegeben. In Zeile 5 als Two-Way-Bindung in einem Eingabefeld, in Zeile 6 als One-Way-Binding.

Schon an diesem simplen Beispiel zeigt sich die Leichtigkeit von AngularJS. Der HTML-Code ist einfach zu verstehen, enthält jedoch keine Logik. Durch das von AngularJS kontrollierte Binding ändert sich die Anzeige in Zeile 6, sobald in Zeile 5 ein Wert geändert wird. Der vergleichbare jQuery-Code wäre deutlich länger!

Listing 1:

<body ng-app="myModule" ng-controller="MyCtrl"> 
  <span>{{list.length}} elements</span> 
  <ul> 
    <li ng-repeat="element in list"> 
    <input type="text" ng-model="element.text"> 
    <span>{{element.text}}</span> 
    </li> 
  </ul> 
</body> 

Listing 2 zeigt den zugehörigen Controller MyCtrl. Als Funktionsparameter wird in Zeile 2 von AngularJS der "Scope" injiziert. Dies demonstriert, wie simpel Dependency Injection in AngularJS ist. "Controller" sind aufgrund von Dependancy Injection sehr gut testbar, worauf bei der Entwicklung von AngularJS von Anfang an sehr großen Wert gelegt wurde. In Zeile 3-5 erfolgt dann eine Zuweisung einer Liste von Objekten an den "Scope".

Listing 2:

angular.module('myModule', []) 
.controller("MyCtrl", function($scope) {   
  $scope.list = [     
    {text:'angluarjs'},     
    {text:'jQuery'}]; 
});

Auch zur Teilmigration?

Obwohl AngularJS eigentlich für Single-Page-Anwendungen gedacht ist, kann es auch für andere Zwecke "missbraucht" werden. Vielleicht will man im ersten Schritt ja nur einen kleinen Teil einer Webseite "hübscher" machen. Mit AngularJS ist dies kein Problem, denn es werden ja nur die von einem Element mit dem Attribut ng-app umschlossenen HTML-Elemente analysiert.

Da AngularJS relativ leichtgewichtig ist und schnell initialisiert wird, kann man es auch zur Gestaltung der Oberflächen einer serverseitigen Webanwendung verwenden. Wenn der JavaScript-Code in Dateien ausgelagert wurde, wird dieser gecached und kostet keine wertvolle Ladezeit. Die Initialisierung von AngularJS nach dem Seitenaufruf geschieht sehr schnell und kann quasi vernachlässigt werden.

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-Schnittstellen [4].

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 Zweiteilung 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.

Listing 3:

@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");               
    }
}

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 mitsendet [6].

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.

Listing 4:

$http({method: 'GET', url: '/show.do'}).   
  success(function(data, status, headers, config) {     
    $scope.data = data;   
  });

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).

Listing 5:

<%@ page contentType="application/json; charset=UTF-8" pageEncoding="UTF-8"%> ${jsonResponse}

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].


Autor

Jan Blankenhorn

Jan Blankenhorn ist als Prokurist der WeigleWilczek GmbH und als Trainer für AngularJS und EclipseRCP bei thecodecampus.de tätig.
>> Weiterlesen
botMessage_toctoc_comments_9210