Continuous Integration mit Java und Javascript, Teil 2: Praktische Umsetzung

Architektur/Methoden  –  0 Kommentare

Viele Webprojekte kombinieren serverseitiges Java mit JavaScript für den Client. Das bringt die Herausforderung mit sich, dass die Entwickler Java-Server mit JavaScript-Projekten integrieren und reproduzierbar bauen müssen – am besten permanent.

Nach wie vor verwenden viele Kundenprojekte für die Implementierung von Services Java als Basis – vermutlich aus Gründen des Investitionsschutzes. Bei (Web-)Client-Technologien steht die einfache Nutzbarkeit der Software im Vordergrund. Anfangs lieferte der Server die Webseiten bei jeder Aktion des Nutzers neu aus, aber seit vielen Jahren verfolgen Internetkonzerne wie Google und Facebook das Konzept der Single Page Application, also dem Routing der Webseiten einer Applikation innerhalb des Browsers. Frameworks wie das populäre AngularJS unterstützen die Entwicklung solch komplexer und in JavaScript implementierter Web-Clients.

Um die Integration praktisch lösen zu können, werden zuerst die Unterschiede der Tool-Chains und im Anschluss eine Projektstruktur beschrieben, die sich reproduzierbar bauen lässt. Sie ist flexibel genug, um JavaScript- und Java-Tools gleichzeitig nutzen zu können.

Wie im ersten Teil erläutert, entscheidet die Organisationsstruktur über die Projektstruktur. Daher werden im Folgenden einige Annahmen über den Aufbau des Projekts gemacht:

  • Mindestens zwei Mitarbeiter oder Teams entwickeln das Projekt: ein Frontend- und ein Java-Backend-Entwickler
  • Die Webapplikation soll auf einem Java-EE-Server laufen, weil die Geschäftsführung eine strategische Entscheidung getroffen hat.
  • Für die Java-Entwicklung ist ein Tool wie Gradle oder Maven im Einsatz.
  • Es gibt eine integrierte Projektstruktur, um lange Build-Ketten und das Problem der Propagierung von Versionen zu vermeiden.

Die Aufgabe wäre nun, einen reproduzierbaren und effizienten CI-Workflow für ein fehlerfreies Continuous Delivery zu entwerfen. Das Anschauungsbeispiel wurde auf GitHub veröffentlicht.

Jedes Projekt hat seine eigene Anatomie

In einem typischen Java-Projekt erzeugt der Compiler Bytecode für eine bestimmte und standardisierte Ausführungsstruktur, etwa ein WAR- oder EAR-Archiv. Durch Tools wie Maven oder Gradle ähneln sich die Projektstrukturen im Java-Universum.

Continuous Integration

Teil 1: Die Kunst der Versionierung

Teil 2: Praktische Umsetzung

Im JavaScript-Umfeld findet sich augenscheinlich keine ähnliche Einheitlichkeit. Durchgesetzt hat sich bei der Entwicklung von AngularJS-Anwendungen hingegen das Paketier-und-Build-System npm, das mit Node.js vorinstalliert wird. Letzteres ist eine JavaScript-Ausführungsumgebung auf der Basis von Googles V8 und Grundlage der meisten Tools im Javascript-Umfeld, aber leider nicht das einzige Werkzeug.

Die typischen Schritte in einem AngularJS-Projekt sind das Generieren von CSS- und HTML-Dateien, das Transpilieren/Kompilieren nach JavaScript, die Minifizierung der Dateien und die Verkettung mehrerer Files. Die hierfür zur Verfügung stehenden Tools benötigen je nach Anwendungsfall unterschiedliche Ausführumgebungen. So nutzt beispielsweise der populäre CSS-Generator Sass (Syntactically Awesome Stylesheets) primär Ruby für die Erzeugung der Stylesheets. Der Paketmanager npm ist in manchen Fällen gezwungen, C/C++-Code je nach Betriebssystem mithilfe von gyp zu übersetzen, einem in Python geschriebenen Bau-Tool. Und sogar die Java-Umgebung ist notwendig, wenn das Team mit Selenium Ende-zu-Ende-Tests durchführt.

Im Interesse einer geringen Anzahl benötigter Tools läge es auf der Hand, die JavaScript-Artefakte mit einem Java-Build-Tool zu erzeugen. Es existieren für Gradle und Maven entsprechende Plug-ins (gradle-compass, gradle-js-plugin), aber leider nicht für alle Bereiche. Für die Durchführung von Unit-Tests, E2E-Tests oder dem Annotieren der JavaScript-Dateien zum Schutz vor der Minifizierung (ng-annotate) existieren keine passenden Werkzeuge in Java. Der Grund dafür ist, dass JavaScript-Entwickler ihre Tools eher in ihrer eigenen Sprache entwickeln. Um die Pflege eines Java-Paralleluniversums für Werkzeuge zu vermeiden, ist die Integration der JavaScript-Tools in einem Java-Build daher manchmal der bessere Weg.

Praktischerweise existiert für Gradle ein Plug-in, das Node.js nicht nur einfach ausführt, sondern auch in der Lage ist, eine spezifische Version zu installieren. Zusammen mit dem Wrapper-Mechanismus von Gradle sind sämtliche Build-Tools zum Bauzeitpunkt deterministisch verfügbar (Versionsstabilität). Hierzu müssen die Entwickler in der Datei ./build.gradle einen Wrapper Task hinzufügen und im client-Verzeichnis das Node-Plug-in konfigurieren, wie folgende beiden Code-Beispieler demonstrieren:

task wrapper(type: Wrapper) {
gradleVersion = '2.3'
}
node {
version = '0.12.0'
download = true
}

Beim Aufruf von Gradle über ./gradlew werden sowohl Gradle als auch Node.js in der angegebenen Version installiert. Konfigurationsoptionen bestimmen dabei sogar optional die Quelle, sodass ein Zugriff auf das Internet nicht erforderlich ist.

Das Anschauungsbeispiel ist in vier Teile geschnitten und folgendermaßen konfiguriert:

rootProject.name = 'triton'
include 'common'
include 'client'
include 'service'
include 'server'

Im "common"-Projekt befinden sich hauptsächlich Schnittstellen, die das "service"-Modul benutzt. Letzteres enthält die aufgerufene Rest-Schnittstelle:

@Path("/product")
public class Products implements ProductService {
@GET
@Path("/echo")
@Produces(MediaType.APPLICATION_JSON)
public String echo(String text) {
return text;
}
//...
}

Im "client"-Modul befindet sich die AngularJS-Applikation inklusive Tests. Alle genannten Gradle-Projekte exportieren ein JAR-Archiv, die im "server"-Projekt zu einem WAR-Archiv zusammengesetzt werden.